From 4449015226aee927585db75eae2605203f52221e Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 17:50:42 -0600 Subject: [PATCH 001/204] Meeting minutes: added minutes from 2 Feb --- .../Comp 4350 Group 6 - Meeting 4 - 2 Feb.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 meeting-minutes/Comp 4350 Group 6 - Meeting 4 - 2 Feb.md diff --git a/meeting-minutes/Comp 4350 Group 6 - Meeting 4 - 2 Feb.md b/meeting-minutes/Comp 4350 Group 6 - Meeting 4 - 2 Feb.md new file mode 100644 index 0000000..447fada --- /dev/null +++ b/meeting-minutes/Comp 4350 Group 6 - Meeting 4 - 2 Feb.md @@ -0,0 +1,18 @@ +5/6 present - quorum + +### Agenda + +- Recap +- Discussion on architecture and tools +- Job assignment + +### Recap: + +Meeting with TA was discussed. The topics of load balancing and slight changes in architecture were discussed. + +### Jobs:  + +- Visualization dashboard - Roman, Huy +- User Auth - Deep, Logan +- User data input - Dylan Deep, Jackie +- Setup DB schema - Logan From 7aad80f614a9d35bd894a637ee46b7a0544296e0 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 19:37:09 -0600 Subject: [PATCH 002/204] Client setup: initialized client skeleton from template with vite --- app/client/.gitignore | 24 ++ app/client/README.md | 73 +++++ app/client/bun.lock | 459 ++++++++++++++++++++++++++++++++ app/client/eslint.config.js | 23 ++ app/client/index.html | 13 + app/client/package.json | 33 +++ app/client/public/vite.svg | 1 + app/client/src/App.css | 42 +++ app/client/src/App.tsx | 46 ++++ app/client/src/assets/react.svg | 1 + app/client/src/index.css | 68 +++++ app/client/src/main.tsx | 10 + app/client/tsconfig.app.json | 28 ++ app/client/tsconfig.json | 7 + app/client/tsconfig.node.json | 26 ++ app/client/vite.config.ts | 7 + 16 files changed, 861 insertions(+) create mode 100644 app/client/.gitignore create mode 100644 app/client/README.md create mode 100644 app/client/bun.lock create mode 100644 app/client/eslint.config.js create mode 100644 app/client/index.html create mode 100644 app/client/package.json create mode 100644 app/client/public/vite.svg create mode 100644 app/client/src/App.css create mode 100644 app/client/src/App.tsx create mode 100644 app/client/src/assets/react.svg create mode 100644 app/client/src/index.css create mode 100644 app/client/src/main.tsx create mode 100644 app/client/tsconfig.app.json create mode 100644 app/client/tsconfig.json create mode 100644 app/client/tsconfig.node.json create mode 100644 app/client/vite.config.ts diff --git a/app/client/.gitignore b/app/client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/app/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/app/client/README.md b/app/client/README.md new file mode 100644 index 0000000..4dcad1f --- /dev/null +++ b/app/client/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/app/client/bun.lock b/app/client/bun.lock new file mode 100644 index 0000000..2581ecc --- /dev/null +++ b/app/client/bun.lock @@ -0,0 +1,459 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "finus-client", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "npm:rolldown-vite@7.2.5", + }, + }, + }, + "overrides": { + "vite": "npm:rolldown-vite@7.2.5", + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.0", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.97.0", "", {}, "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w=="], + + "@oxc-project/types": ["@oxc-project/types@0.97.0", "", {}, "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "x64" }, "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.50", "", { "os": "freebsd", "cpu": "x64" }, "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm" }, "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.50", "", { "os": "none", "cpu": "arm64" }, "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.50", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "arm64" }, "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA=="], + + "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "ia32" }, "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "x64" }, "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], + + "@swc/core": ["@swc/core@1.15.11", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.11", "@swc/core-darwin-x64": "1.15.11", "@swc/core-linux-arm-gnueabihf": "1.15.11", "@swc/core-linux-arm64-gnu": "1.15.11", "@swc/core-linux-arm64-musl": "1.15.11", "@swc/core-linux-x64-gnu": "1.15.11", "@swc/core-linux-x64-musl": "1.15.11", "@swc/core-win32-arm64-msvc": "1.15.11", "@swc/core-win32-ia32-msvc": "1.15.11", "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.11", "", { "os": "linux", "cpu": "arm" }, "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.11", "", { "os": "win32", "cpu": "x64" }, "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + + "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="], + + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.2.3", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "vite": ["rolldown-vite@7.2.5", "", { "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.50", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + } +} diff --git a/app/client/eslint.config.js b/app/client/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/app/client/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/app/client/index.html b/app/client/index.html new file mode 100644 index 0000000..829f295 --- /dev/null +++ b/app/client/index.html @@ -0,0 +1,13 @@ + + + + + + + finus-client + + +
+ + + diff --git a/app/client/package.json b/app/client/package.json new file mode 100644 index 0000000..53d79f9 --- /dev/null +++ b/app/client/package.json @@ -0,0 +1,33 @@ +{ + "name": "finus-client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "npm:rolldown-vite@7.2.5" + }, + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } +} diff --git a/app/client/public/vite.svg b/app/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/app/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/App.css b/app/client/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/app/client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx new file mode 100644 index 0000000..5d373b7 --- /dev/null +++ b/app/client/src/App.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +// broadcast to the API gate endpoint - should receive responses from all services that are set up +async function testAPI() { + const response = await fetch('http://localhost:3000/api/test').then(res => res.text()) + return response +} + +function App() { + const [count, setCount] = useState(0) + const [response, setResponse] = useState('') + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+ +

Response from server: {response}

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/app/client/src/assets/react.svg b/app/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/app/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/index.css b/app/client/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/app/client/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/app/client/src/main.tsx b/app/client/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/app/client/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/app/client/tsconfig.app.json b/app/client/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/app/client/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/app/client/tsconfig.json b/app/client/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/app/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/app/client/tsconfig.node.json b/app/client/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/app/client/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/app/client/vite.config.ts b/app/client/vite.config.ts new file mode 100644 index 0000000..2328e17 --- /dev/null +++ b/app/client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From 55526bbdfbe0b86964352e80909547aac7a2eb91 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 20:04:58 -0600 Subject: [PATCH 003/204] API Gatewat Service: initialized with test api endpoint --- app/server/api-gateway-s/.gitignore | 34 ++++ app/server/api-gateway-s/Dockerfile | 12 ++ app/server/api-gateway-s/README.md | 15 ++ app/server/api-gateway-s/bun.lock | 213 +++++++++++++++++++++++++ app/server/api-gateway-s/package.json | 19 +++ app/server/api-gateway-s/src/index.ts | 44 +++++ app/server/api-gateway-s/tsconfig.json | 29 ++++ 7 files changed, 366 insertions(+) create mode 100644 app/server/api-gateway-s/.gitignore create mode 100644 app/server/api-gateway-s/Dockerfile create mode 100644 app/server/api-gateway-s/README.md create mode 100644 app/server/api-gateway-s/bun.lock create mode 100644 app/server/api-gateway-s/package.json create mode 100644 app/server/api-gateway-s/src/index.ts create mode 100644 app/server/api-gateway-s/tsconfig.json diff --git a/app/server/api-gateway-s/.gitignore b/app/server/api-gateway-s/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/app/server/api-gateway-s/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/app/server/api-gateway-s/Dockerfile b/app/server/api-gateway-s/Dockerfile new file mode 100644 index 0000000..f38333c --- /dev/null +++ b/app/server/api-gateway-s/Dockerfile @@ -0,0 +1,12 @@ +FROM oven/bun:1.0-slim + +WORKDIR /app + +COPY package.json ./ +RUN bun install + +COPY . . + +EXPOSE 3000 + +CMD ["bun", "src/index.ts"] \ No newline at end of file diff --git a/app/server/api-gateway-s/README.md b/app/server/api-gateway-s/README.md new file mode 100644 index 0000000..818da4b --- /dev/null +++ b/app/server/api-gateway-s/README.md @@ -0,0 +1,15 @@ +# api-gateway-s + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/api-gateway-s/bun.lock b/app/server/api-gateway-s/bun.lock new file mode 100644 index 0000000..cd88da8 --- /dev/null +++ b/app/server/api-gateway-s/bun.lock @@ -0,0 +1,213 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "api-gateway-s", + "dependencies": { + "@types/express": "^5.0.6", + "cors": "^2.8.6", + "express": "^5.2.1", + "http-proxy-middleware": "^3.0.5", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^25.2.0", + "typescript": "^5.3.3", + }, + }, + }, + "packages": { + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="], + + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], + + "http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + } +} diff --git a/app/server/api-gateway-s/package.json b/app/server/api-gateway-s/package.json new file mode 100644 index 0000000..565fd1c --- /dev/null +++ b/app/server/api-gateway-s/package.json @@ -0,0 +1,19 @@ +{ + "name": "api-gateway-s", + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "tsc", + "start": "bun dist/index.js" + }, + "dependencies": { + "@types/express": "^5.0.6", + "cors": "^2.8.6", + "express": "^5.2.1", + "http-proxy-middleware": "^3.0.5" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^25.2.0", + "typescript": "^5.3.3" + } +} diff --git a/app/server/api-gateway-s/src/index.ts b/app/server/api-gateway-s/src/index.ts new file mode 100644 index 0000000..593cae9 --- /dev/null +++ b/app/server/api-gateway-s/src/index.ts @@ -0,0 +1,44 @@ +/* +This service should have express, cors and http-proxy-middleware installed as dependencies. +It will act as an API Gateway, routing requests to the appropriate microservices. In the real world, the other + microservices would not be reachable from the outside internet, but for now this will do. + +The /api/test endpoint will try to reach every other stood up service and return their status to client. + +*/ + +import express from 'express'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { json } from 'stream/consumers'; + +const app = express(); +const PORT = process.env.PORT || 3000; + + +interface Dictionary { + [key: string]: string; +} + +app.use(express.json()); + +app.listen(PORT, () => { + console.log(`API Gateway running on port ${PORT} at http://localhost:${PORT}`); +}); + +//test endpoint +app.get('/api/test', (req: express.Request, res: express.Response) => { + console.log('Received test request at API Gateway: ', req.url); + let output: Dictionary = {}; + + output['api-gateway'] = `ok`; + let services: string[] = ['analytics', 'user', 'authentication', 'market']; + + for (let i = 1; i < 5; i++) { + // reach out to each microservice + const service = services[i - 1]; + if (service) { + output[service] = `http://localhost:300${i}/api/test`; + } + } + res.json(output); +}); \ No newline at end of file diff --git a/app/server/api-gateway-s/tsconfig.json b/app/server/api-gateway-s/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/app/server/api-gateway-s/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From 2e22aae2080e97597a996a6eb27fb74f2f5a43ef Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 20:12:14 -0600 Subject: [PATCH 004/204] Auth Service: initialized service with test api endpoint --- app/server/auth-s/.gitignore | 34 +++++ app/server/auth-s/Dockerfile | 12 ++ app/server/auth-s/README.md | 15 +++ app/server/auth-s/bun.lock | 223 ++++++++++++++++++++++++++++++++ app/server/auth-s/package.json | 26 ++++ app/server/auth-s/src/index.ts | 15 +++ app/server/auth-s/tsconfig.json | 29 +++++ 7 files changed, 354 insertions(+) create mode 100644 app/server/auth-s/.gitignore create mode 100644 app/server/auth-s/Dockerfile create mode 100644 app/server/auth-s/README.md create mode 100644 app/server/auth-s/bun.lock create mode 100644 app/server/auth-s/package.json create mode 100644 app/server/auth-s/src/index.ts create mode 100644 app/server/auth-s/tsconfig.json diff --git a/app/server/auth-s/.gitignore b/app/server/auth-s/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/app/server/auth-s/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/app/server/auth-s/Dockerfile b/app/server/auth-s/Dockerfile new file mode 100644 index 0000000..136cfb3 --- /dev/null +++ b/app/server/auth-s/Dockerfile @@ -0,0 +1,12 @@ +FROM oven/bun:1.0-slim + +WORKDIR /app + +COPY package.json ./ +RUN bun install + +COPY . . + +EXPOSE 3001 + +CMD ["bun", "src/index.ts"] \ No newline at end of file diff --git a/app/server/auth-s/README.md b/app/server/auth-s/README.md new file mode 100644 index 0000000..e0b08cd --- /dev/null +++ b/app/server/auth-s/README.md @@ -0,0 +1,15 @@ +# auth-s + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/auth-s/bun.lock b/app/server/auth-s/bun.lock new file mode 100644 index 0000000..f40f72b --- /dev/null +++ b/app/server/auth-s/bun.lock @@ -0,0 +1,223 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "auth-s", + "dependencies": { + "bcrypt": "^6.0.0", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + }, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/bun": "latest", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^25.1.0", + }, + "peerDependencies": { + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + } +} diff --git a/app/server/auth-s/package.json b/app/server/auth-s/package.json new file mode 100644 index 0000000..4cfeda6 --- /dev/null +++ b/app/server/auth-s/package.json @@ -0,0 +1,26 @@ +{ + "name": "auth-s", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/bun": "latest", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^25.1.0" + }, + "peerDependencies": { + "typescript": "^5.9.3" + }, + "dependencies": { + "bcrypt": "^6.0.0", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3" + }, + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "tsc", + "start": "bun dist/index.js" + } +} diff --git a/app/server/auth-s/src/index.ts b/app/server/auth-s/src/index.ts new file mode 100644 index 0000000..2b053cc --- /dev/null +++ b/app/server/auth-s/src/index.ts @@ -0,0 +1,15 @@ +import express from 'express'; +const app = express(); +const PORT = process.env.PORT || 3001; + +app.use(express.json()); + +app.listen(PORT, () => { + console.log(`Authentication Service running on port ${PORT} at http://localhost:${PORT}`); +}); + +//test endpoint +app.get('/api/test', (req: express.Request, res: express.Response) => { + console.log('Received test request at Authentication Service: ', req.url); + res.send('ok'); +}); \ No newline at end of file diff --git a/app/server/auth-s/tsconfig.json b/app/server/auth-s/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/app/server/auth-s/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From 6f1c062cbeb69dd4ed3bf8dee0fc346c42043fb0 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 20:16:13 -0600 Subject: [PATCH 005/204] User Service: initialized with test api endpoint --- app/server/user-s/.gitignore | 34 ++++++ app/server/user-s/Dockerfile | 12 +++ app/server/user-s/README.md | 15 +++ app/server/user-s/bun.lock | 179 ++++++++++++++++++++++++++++++++ app/server/user-s/package.json | 22 ++++ app/server/user-s/src/index.ts | 15 +++ app/server/user-s/tsconfig.json | 29 ++++++ 7 files changed, 306 insertions(+) create mode 100644 app/server/user-s/.gitignore create mode 100644 app/server/user-s/Dockerfile create mode 100644 app/server/user-s/README.md create mode 100644 app/server/user-s/bun.lock create mode 100644 app/server/user-s/package.json create mode 100644 app/server/user-s/src/index.ts create mode 100644 app/server/user-s/tsconfig.json diff --git a/app/server/user-s/.gitignore b/app/server/user-s/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/app/server/user-s/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/app/server/user-s/Dockerfile b/app/server/user-s/Dockerfile new file mode 100644 index 0000000..8746464 --- /dev/null +++ b/app/server/user-s/Dockerfile @@ -0,0 +1,12 @@ +FROM oven/bun:1.0-slim + +WORKDIR /app + +COPY package.json ./ +RUN bun install + +COPY . . + +EXPOSE 3002 + +CMD ["bun", "src/index.ts"] \ No newline at end of file diff --git a/app/server/user-s/README.md b/app/server/user-s/README.md new file mode 100644 index 0000000..cca60c8 --- /dev/null +++ b/app/server/user-s/README.md @@ -0,0 +1,15 @@ +# user-s + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/user-s/bun.lock b/app/server/user-s/bun.lock new file mode 100644 index 0000000..1090b99 --- /dev/null +++ b/app/server/user-s/bun.lock @@ -0,0 +1,179 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "user-s", + "dependencies": { + "express": "^5.2.1", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^5.0.6", + "@types/node": "^25.1.0", + }, + "peerDependencies": { + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + } +} diff --git a/app/server/user-s/package.json b/app/server/user-s/package.json new file mode 100644 index 0000000..6c840b5 --- /dev/null +++ b/app/server/user-s/package.json @@ -0,0 +1,22 @@ +{ + "name": "user-s", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^5.0.6", + "@types/node": "^25.1.0" + }, + "peerDependencies": { + "typescript": "^5.9.3" + }, + "dependencies": { + "express": "^5.2.1" + }, + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "tsc", + "start": "bun dist/index.js" + } +} \ No newline at end of file diff --git a/app/server/user-s/src/index.ts b/app/server/user-s/src/index.ts new file mode 100644 index 0000000..319e1a7 --- /dev/null +++ b/app/server/user-s/src/index.ts @@ -0,0 +1,15 @@ +import express from 'express'; +const app = express(); +const PORT = process.env.PORT || 3002; + +app.use(express.json()); + +app.listen(PORT, () => { + console.log(`User Service running on port ${PORT} at http://localhost:${PORT}`); +}); + +//test endpoint +app.get('/api/test', (req: express.Request, res: express.Response) => { + console.log('Received test request at User Service: ', req.url); + res.send('ok'); +}); \ No newline at end of file diff --git a/app/server/user-s/tsconfig.json b/app/server/user-s/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/app/server/user-s/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From 72c4daad0291737907fff4d30e467ec4c0155533 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 20:40:45 -0600 Subject: [PATCH 006/204] Python Services: added the analytics and market services with their initial test api enpoints --- app/server/analytics-s/Dockerfile | 18 +++++++++++ app/server/analytics-s/requirements.txt | 6 ++++ app/server/analytics-s/src/main.py | 29 ++++++++++++++++++ app/server/api-gateway-s/src/index.ts | 12 +++++--- app/server/docker-compose.yml | 40 +++++++++++++++++++++++++ app/server/market-s/Dockerfile | 18 +++++++++++ app/server/market-s/requirements.txt | 5 ++++ app/server/market-s/src/main.py | 14 +++++++++ 8 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 app/server/analytics-s/Dockerfile create mode 100644 app/server/analytics-s/requirements.txt create mode 100644 app/server/analytics-s/src/main.py create mode 100644 app/server/docker-compose.yml create mode 100644 app/server/market-s/Dockerfile create mode 100644 app/server/market-s/requirements.txt create mode 100644 app/server/market-s/src/main.py diff --git a/app/server/analytics-s/Dockerfile b/app/server/analytics-s/Dockerfile new file mode 100644 index 0000000..b969fd6 --- /dev/null +++ b/app/server/analytics-s/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 3003 + +CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3003"] \ No newline at end of file diff --git a/app/server/analytics-s/requirements.txt b/app/server/analytics-s/requirements.txt new file mode 100644 index 0000000..8d18418 --- /dev/null +++ b/app/server/analytics-s/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +pandas==2.1.4 +numpy==1.26.3 +scikit-learn==1.3.2 \ No newline at end of file diff --git a/app/server/analytics-s/src/main.py b/app/server/analytics-s/src/main.py new file mode 100644 index 0000000..902bee3 --- /dev/null +++ b/app/server/analytics-s/src/main.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from fastapi.middleware.cors import CORSMiddleware +import pandas as pd +import numpy as np +import uvicorn +import os + +app = FastAPI(title ="Analytics Service") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# test endpoint +@app.get('/api/test') +def test_endpoint(): + return 'ok' + + + +if __name__ == "__main__": + port = int(os.getenv("PORT", 3003)) + uvicorn.run(app, host="0.0.0.0", port=port) \ No newline at end of file diff --git a/app/server/api-gateway-s/src/index.ts b/app/server/api-gateway-s/src/index.ts index 593cae9..4d61777 100644 --- a/app/server/api-gateway-s/src/index.ts +++ b/app/server/api-gateway-s/src/index.ts @@ -9,7 +9,6 @@ The /api/test endpoint will try to reach every other stood up service and return import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { json } from 'stream/consumers'; const app = express(); const PORT = process.env.PORT || 3000; @@ -26,7 +25,7 @@ app.listen(PORT, () => { }); //test endpoint -app.get('/api/test', (req: express.Request, res: express.Response) => { +app.get('/api/test', async (req: express.Request, res: express.Response) => { console.log('Received test request at API Gateway: ', req.url); let output: Dictionary = {}; @@ -37,8 +36,13 @@ app.get('/api/test', (req: express.Request, res: express.Response) => { // reach out to each microservice const service = services[i - 1]; if (service) { - output[service] = `http://localhost:300${i}/api/test`; + try { + const response = await fetch(`http://localhost:300${i}/api/test`); + output[service] = await response.text(); + } catch (error) { + output[service] = `error`; + } } } res.json(output); -}); \ No newline at end of file +}); diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml new file mode 100644 index 0000000..3b4d25b --- /dev/null +++ b/app/server/docker-compose.yml @@ -0,0 +1,40 @@ +version: '3.8' +services: + # API gateway + api-gateway-s: + build: ./api-gateway-s + ports: + - "3000:3000" + environment: + - NODE_ENV=development + depends_on: + - auth-s + - user-s + + # auth service + auth-s: + build: ./auth-s + environment: + - PORT=3001 + + # user service + user-s: + build: ./user-s + environment: + - PORT=3002 + + # python analytics service + analytics-s: + build: ./analytics-s + ports: + - "3003:3003" + environment: + - PORT=3003 + + # python market service + market-service: + build: ./market-s + ports: + - "3004:3004" + environment: + - PORT=3004 \ No newline at end of file diff --git a/app/server/market-s/Dockerfile b/app/server/market-s/Dockerfile new file mode 100644 index 0000000..eec9c0d --- /dev/null +++ b/app/server/market-s/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 3004 + +CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3004"] \ No newline at end of file diff --git a/app/server/market-s/requirements.txt b/app/server/market-s/requirements.txt new file mode 100644 index 0000000..0ac6586 --- /dev/null +++ b/app/server/market-s/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +yfinance==0.2.33 +pandas==2.1.4 +requests==2.31.0 \ No newline at end of file diff --git a/app/server/market-s/src/main.py b/app/server/market-s/src/main.py new file mode 100644 index 0000000..9b37616 --- /dev/null +++ b/app/server/market-s/src/main.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, HTTPException +import uvicorn +import os + +app = FastAPI(title="Market Service") + +@app.get('/api/test') +def test_endpoint(): + return 'ok' + +if __name__ == "__main__": + + port = int(os.getenv("PORT", 3004)) + uvicorn.run(app, host="0.0.0.0", port=port) \ No newline at end of file From 613a535c8b2991ee949e2dd0434b3be46a7f8826 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 21:45:06 -0600 Subject: [PATCH 007/204] API Gateway: fixed test endpoint to also run correctly on docker --- app/server/analytics-s/src/main.py | 2 +- app/server/api-gateway-s/src/index.ts | 19 ++++++++++++++++--- app/server/docker-compose.yml | 9 ++++++++- app/server/market-s/src/main.py | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/server/analytics-s/src/main.py b/app/server/analytics-s/src/main.py index 902bee3..5165025 100644 --- a/app/server/analytics-s/src/main.py +++ b/app/server/analytics-s/src/main.py @@ -6,7 +6,7 @@ import uvicorn import os -app = FastAPI(title ="Analytics Service") +app = FastAPI() app.add_middleware( CORSMiddleware, diff --git a/app/server/api-gateway-s/src/index.ts b/app/server/api-gateway-s/src/index.ts index 4d61777..160a29e 100644 --- a/app/server/api-gateway-s/src/index.ts +++ b/app/server/api-gateway-s/src/index.ts @@ -13,6 +13,7 @@ import { createProxyMiddleware } from 'http-proxy-middleware'; const app = express(); const PORT = process.env.PORT || 3000; +const runningOnDocker = process.env.DOCKER_ENV === 'true'; interface Dictionary { [key: string]: string; @@ -21,9 +22,11 @@ interface Dictionary { app.use(express.json()); app.listen(PORT, () => { - console.log(`API Gateway running on port ${PORT} at http://localhost:${PORT}`); + console.log(`API Gateway running on port ${PORT} at http://localhost:${PORT}, Running on Docker: ${runningOnDocker}`); }); + + //test endpoint app.get('/api/test', async (req: express.Request, res: express.Response) => { console.log('Received test request at API Gateway: ', req.url); @@ -32,15 +35,25 @@ app.get('/api/test', async (req: express.Request, res: express.Response) => { output['api-gateway'] = `ok`; let services: string[] = ['analytics', 'user', 'authentication', 'market']; + if (runningOnDocker) { + services = [ 'auth-s', 'user-s', 'analytics-s', 'market-s']; + } + for (let i = 1; i < 5; i++) { // reach out to each microservice const service = services[i - 1]; if (service) { try { - const response = await fetch(`http://localhost:300${i}/api/test`); - output[service] = await response.text(); + if (!runningOnDocker) { + const response = await fetch(`http://localhost:300${i}/api/test`); + output[service] = await response.text(); + } else { + const response = await fetch(`http://${service}:300${i}/api/test`); + output[service] = await response.text(); + } } catch (error) { output[service] = `error`; + console.log(error); } } } diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 3b4d25b..83e2f05 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -7,19 +7,26 @@ services: - "3000:3000" environment: - NODE_ENV=development + - DOCKER_ENV=true depends_on: - auth-s - user-s + - analytics-s + - market-s # auth service auth-s: build: ./auth-s + ports: + - "3001:3001" environment: - PORT=3001 # user service user-s: build: ./user-s + ports: + - "3002:3002" environment: - PORT=3002 @@ -32,7 +39,7 @@ services: - PORT=3003 # python market service - market-service: + market-s: build: ./market-s ports: - "3004:3004" diff --git a/app/server/market-s/src/main.py b/app/server/market-s/src/main.py index 9b37616..f3554e0 100644 --- a/app/server/market-s/src/main.py +++ b/app/server/market-s/src/main.py @@ -2,7 +2,7 @@ import uvicorn import os -app = FastAPI(title="Market Service") +app = FastAPI() @app.get('/api/test') def test_endpoint(): From fb7224c88b37a6ef939aada50cd2825645227256 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 2 Feb 2026 21:53:08 -0600 Subject: [PATCH 008/204] API Gateway: added cors to allow Vite's default dev port to access port 3000 (the one that the api service is running on) --- app/server/api-gateway-s/bun.lock | 3 +++ app/server/api-gateway-s/package.json | 1 + app/server/api-gateway-s/src/index.ts | 16 +++++++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/server/api-gateway-s/bun.lock b/app/server/api-gateway-s/bun.lock index cd88da8..8bff853 100644 --- a/app/server/api-gateway-s/bun.lock +++ b/app/server/api-gateway-s/bun.lock @@ -5,6 +5,7 @@ "": { "name": "api-gateway-s", "dependencies": { + "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "cors": "^2.8.6", "express": "^5.2.1", @@ -24,6 +25,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], diff --git a/app/server/api-gateway-s/package.json b/app/server/api-gateway-s/package.json index 565fd1c..1fe1901 100644 --- a/app/server/api-gateway-s/package.json +++ b/app/server/api-gateway-s/package.json @@ -6,6 +6,7 @@ "start": "bun dist/index.js" }, "dependencies": { + "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "cors": "^2.8.6", "express": "^5.2.1", diff --git a/app/server/api-gateway-s/src/index.ts b/app/server/api-gateway-s/src/index.ts index 160a29e..229d076 100644 --- a/app/server/api-gateway-s/src/index.ts +++ b/app/server/api-gateway-s/src/index.ts @@ -9,15 +9,25 @@ The /api/test endpoint will try to reach every other stood up service and return import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; +import cors from 'cors'; + +interface Dictionary { + [key: string]: string; +} const app = express(); const PORT = process.env.PORT || 3000; +app.use(cors({ + origin: 'http://localhost:5173', // vite dev server so that the client can access this API + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'] +})); + const runningOnDocker = process.env.DOCKER_ENV === 'true'; -interface Dictionary { - [key: string]: string; -} + app.use(express.json()); From 536cac559c818c8e4039612cfc8a0bd3a43c851f Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Tue, 3 Feb 2026 11:38:41 -0600 Subject: [PATCH 009/204] Client: setup routing to pages --- app/client/bun.lock | 16 +++++ app/client/package.json | 4 +- app/client/src/App.tsx | 61 ++++++++++++++----- .../src/components/ui/LoadingSpinner.tsx | 7 +++ 4 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 app/client/src/components/ui/LoadingSpinner.tsx diff --git a/app/client/bun.lock b/app/client/bun.lock index 2581ecc..a786de4 100644 --- a/app/client/bun.lock +++ b/app/client/bun.lock @@ -5,8 +5,10 @@ "": { "name": "finus-client", "dependencies": { + "@types/react-router-dom": "^5.3.3", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -168,6 +170,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/history": ["@types/history@4.7.11", "", {}, "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], @@ -176,6 +180,10 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-router": ["@types/react-router@5.1.20", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*" } }, "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q=="], + + "@types/react-router-dom": ["@types/react-router-dom@5.3.3", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "*" } }, "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="], @@ -230,6 +238,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -392,6 +402,10 @@ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], + + "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="], @@ -400,6 +414,8 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], diff --git a/app/client/package.json b/app/client/package.json index 53d79f9..151585f 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@types/react-router-dom": "^5.3.3", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 5d373b7..9f304c7 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -1,28 +1,49 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' +/* +This is to be used as a rerouter to various website pages. +*/ + +import { Suspense, useState, lazy} from 'react' import './App.css' +import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom' +import LoadingSpinner from './components/ui/LoadingSpinner.tsx' -// broadcast to the API gate endpoint - should receive responses from all services that are set up -async function testAPI() { - const response = await fetch('http://localhost:3000/api/test').then(res => res.text()) - return response +function App() { + return ( + +
+ + ) } -function App() { + + +function Main() { + const location = useLocation() + + return ( +
+ {/* Conditionally render Navbar - adjust logic as needed */} + {location.pathname !== '/' && location.pathname !== '/login'} + + }> + + {/* } /> + } /> */} + {/* Add a 404 page */} + } /> + + +
+ ) +} + +//default page for 404 - this can test the APIs for now +function NotFoundPage() { const [count, setCount] = useState(0) const [response, setResponse] = useState('') return ( <> -

Vite + React

+
+ + + + + + {status && ( +
+ {status.message} +
+ )} + + {user && ( +
+ Aprroved: Signed in as {user.email} +
+ )} + +
+ +

switch

+ +
+ + + +
+ + ); +} diff --git a/user-auth/client/src/api.js b/user-auth/client/src/api.js new file mode 100644 index 0000000..9785ac4 --- /dev/null +++ b/user-auth/client/src/api.js @@ -0,0 +1,19 @@ +const API_BASE = "http://localhost:4000"; + +export async function signup(email, password) { + const res = await fetch(`${API_BASE}/api/auth/signup`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + return await res.json(); +} + +export async function login(email, password) { + const res = await fetch(`${API_BASE}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + return await res.json(); +} diff --git a/user-auth/client/src/assets/react.svg b/user-auth/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/user-auth/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/user-auth/client/src/index.css b/user-auth/client/src/index.css new file mode 100644 index 0000000..6bd408c --- /dev/null +++ b/user-auth/client/src/index.css @@ -0,0 +1,347 @@ +/* Full-screen app */ +html, body, #root { + height: 100%; + width: 100%; + margin: 0; +} + +:root { + --bg: #0b0f0c; + --card: rgba(15, 26, 18, 0.92); + --border: rgba(42, 90, 61, 0.85); + --text: #eaffef; + --muted: #bfeccc; + --green: #2cff79; + --green2: #7cff9b; +} + +body { + background: var(--bg); + color: var(--text); + font-family: system-ui, Arial, sans-serif; + overflow: hidden; +} + +/* Layout */ +.page { + position: relative; + min-height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 28px; +} + +/* Animated background grid */ +.bg-grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(44, 255, 121, 0.06) 1px, transparent 1px), + linear-gradient(90deg, rgba(44, 255, 121, 0.06) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(circle at 50% 45%, black 0%, transparent 70%); + opacity: 0.9; +} + +/* Floating orbs */ +.orb { + position: absolute; + width: 520px; + height: 520px; + border-radius: 50%; + filter: blur(18px); + opacity: 0.18; + animation: float 10s ease-in-out infinite; + pointer-events: none; +} + +.orb-1 { + background: radial-gradient(circle, rgba(44,255,121,0.85), rgba(0,0,0,0) 60%); + top: 10%; + left: 10%; +} + +.orb-2 { + background: radial-gradient(circle, rgba(124,255,155,0.7), rgba(0,0,0,0) 60%); + bottom: 8%; + right: 10%; + animation-duration: 12s; +} + +@keyframes float { + 0%, 100% { transform: translateY(0px) translateX(0px); } + 50% { transform: translateY(-18px) translateX(12px); } +} + +.wrap { + width: 100%; + display: flex; + justify-content: center; + z-index: 1; +} + +/* Card */ +.card { + position: relative; + width: 100%; + max-width: 470px; + background: var(--card); + border-radius: 26px; + padding: 22px; + box-shadow: 0 18px 60px rgba(0,0,0,0.55); + backdrop-filter: blur(10px); + overflow: hidden; +} + +/* Gradient border ring */ +.card-border { + position: absolute; + inset: 0; + padding: 1px; + border-radius: 26px; + background: linear-gradient( + 135deg, + rgba(44,255,121,0.55), + rgba(31,58,42,0.3), + rgba(124,255,155,0.45) + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; +} + +/* Top area */ +.top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 18px; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; +} + +.logo { + width: 48px; + height: 48px; + border-radius: 16px; + display: grid; + place-items: center; + background: var(--green); + color: #06210f; + font-weight: 900; + font-size: 18px; + box-shadow: 0 12px 30px rgba(44,255,121,0.16); +} + +.brand-title { + margin: 0; + font-size: 28px; + line-height: 1.1; + color: var(--green2); +} + +.brand-sub { + margin: 4px 0 0; + font-size: 13px; + color: var(--muted); +} + +.pill { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--muted); + border: 1px solid rgba(31, 58, 42, 0.95); + background: rgba(11, 18, 13, 0.55); + padding: 8px 10px; + border-radius: 999px; + user-select: none; +} + +.dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 0 6px rgba(44,255,121,0.12); +} + +/* Form */ +.form { + display: grid; + gap: 14px; +} + +.field { + display: grid; + gap: 7px; +} + +.label { + font-size: 13px; + color: #d7ffe2; +} + +.input-shell { + display: flex; + align-items: center; + gap: 8px; + border-radius: 16px; + border: 1px solid var(--border); + background: rgba(11, 18, 13, 0.9); + padding: 10px 12px; + transition: transform 0.12s ease, border-color 0.12s ease; +} + +.input-shell:focus-within { + border-color: rgba(44,255,121,0.75); + transform: translateY(-1px); +} + +.icon { + display: inline-flex; + color: rgba(234, 255, 239, 0.78); +} + +.input { + width: 100%; + border: none; + outline: none; + background: transparent; + color: var(--text); + font-size: 14px; +} + +.icon-btn { + border: none; + background: transparent; + color: rgba(234, 255, 239, 0.78); + cursor: pointer; + padding: 6px; + border-radius: 10px; +} + +.icon-btn:hover { + background: rgba(44,255,121,0.08); +} + +/* Primary button */ +.btn { + position: relative; + margin-top: 6px; + width: 100%; + padding: 12px 14px; + border-radius: 16px; + border: none; + cursor: pointer; + font-weight: 900; + color: #06210f; + background: linear-gradient(180deg, rgba(44,255,121,1), rgba(124,255,155,0.95)); + transition: transform 0.12s ease, filter 0.12s ease; + overflow: hidden; +} + +.btn:hover { transform: translateY(-1px); filter: brightness(1.02); } +.btn:active { transform: translateY(0px) scale(0.99); } +.btn:disabled { opacity: 0.7; cursor: not-allowed; } + +.btn-glow { + position: absolute; + inset: -40px; + background: radial-gradient(circle, rgba(255,255,255,0.25), rgba(255,255,255,0) 55%); + transform: translateX(-55%); + animation: sweep 2.8s ease-in-out infinite; + pointer-events: none; +} + +@keyframes sweep { + 0%, 100% { transform: translateX(-55%); } + 50% { transform: translateX(55%); } +} + +/* Messages */ +.msg { + margin-top: 10px; + padding: 10px 12px; + border-radius: 16px; + font-size: 14px; +} + +.msg-err { + background: rgba(42, 15, 18, 0.9); + border: 1px solid #6d1f2a; + color: #ffd2d7; +} + +.msg-ok { + background: rgba(13, 36, 20, 0.85); + border: 1px solid #1f6d3a; + color: #c8ffd6; +} + +.userbox { + margin-top: 10px; + padding: 10px 12px; + border-radius: 16px; + border: 1px dashed rgba(44,255,121,0.85); +} + +/* Divider + switch */ +.divider { + margin: 18px 0 10px; + display: flex; + align-items: center; + gap: 10px; + opacity: 0.85; +} + +.divider span { + height: 1px; + background: rgba(31,58,42,0.95); + flex: 1; +} + +.divider p { + margin: 0; + font-size: 12px; + color: var(--muted); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.ghost { + width: 100%; + padding: 12px 14px; + border-radius: 16px; + border: 1px solid rgba(42, 90, 61, 0.9); + background: rgba(11, 18, 13, 0.35); + color: var(--muted); + cursor: pointer; + font-weight: 800; + transition: transform 0.12s ease, border-color 0.12s ease; +} + +.ghost:hover { + transform: translateY(-1px); + border-color: rgba(44,255,121,0.6); +} + +.foot { + margin-top: 14px; + font-size: 12px; + color: rgba(191, 236, 204, 0.85); +} + +.foot-muted { + color: rgba(191, 236, 204, 0.65); +} diff --git a/user-auth/client/src/main.jsx b/user-auth/client/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/user-auth/client/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/user-auth/client/tsconfig.json b/user-auth/client/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/user-auth/client/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/user-auth/client/vite.config.js b/user-auth/client/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/user-auth/client/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/user-auth/server/app.db b/user-auth/server/app.db new file mode 100644 index 0000000000000000000000000000000000000000..421f2710047d8f8c028b0db5657ac0ef1001d5b6 GIT binary patch literal 16384 zcmeI(J#X4T7zgk(L}`LVXeUpVZa@kaGy;PmL_!K}+<;(5r zze&GJze>lb>QMD?n@S+HL&xa<=+5W!y-3e*^I3j%sW~B|Z~eJzhO|r`6G0%SloCP` zyzcON9|FJG?%$97txb@lPv6s-ZzOr}nH>DceBliO0uX=z1Rwwb2tWV=5P$##An;Ft zesWjL=Y?Ttj%O^mnei9bQNV5@=1sP{J1+)mQ&*|pe6FcLrIjW;5=Bw? zxQO?R0yYogOJZYv{D|h#DM}rSw!6A|t`2B_(C#z`LwcbOY17bq?Ji%`QM-C!J(0Pl zGo!ltMyK5#uZE^kquahT)Oh&X41$F}w?K&|2t_j3jMf0inAD%;wH(V_tw~HUWLpR;ROK! z2tWV=5P$##AOHafKmY;|fWUtWtey(HL|QG&Aq&D&dwnQyH1XZ6Ik$aJcC74eqg2nj zjjE!QtA(&37p@zn?1U{VooTpWl}fLs)oTO$R;?TF%<|G3=8xyr z^Of*|fB*y_009U<00Izz00bZa0SG|gK?poaO8c7!5x>6w-;vDSgGdlbKmY;|fB*y_ a009U<00Izz00bcLM_^A#iu>`806zh0DzM4` literal 0 HcmV?d00001 diff --git a/user-auth/server/db.ts b/user-auth/server/db.ts new file mode 100644 index 0000000..c31ca83 --- /dev/null +++ b/user-auth/server/db.ts @@ -0,0 +1,21 @@ +import { Database } from "bun:sqlite"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +export const db = new Database("app.db"); + +// Run schema on startup +const schemaPath = join(import.meta.dir, "schema.sql"); +const schema = readFileSync(schemaPath, "utf-8"); +db.exec(schema); + +// Prepared statements +export const stmts = { + findByEmail: db.prepare( + "SELECT id, email, password_hash FROM users WHERE email = ?" + ), + + createUser: db.prepare( + "INSERT INTO users (email, password_hash) VALUES (?, ?)" + ), +}; diff --git a/user-auth/server/index.ts b/user-auth/server/index.ts new file mode 100644 index 0000000..3c87275 --- /dev/null +++ b/user-auth/server/index.ts @@ -0,0 +1,113 @@ +import { stmts } from "./db"; + +// ---- helpers ---- +function json(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "http://localhost:5173", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET,POST,OPTIONS", + }, + }); +} + +function badRequest(message: string) { + return json({ ok: false, error: message }, 400); +} + +function isValidEmail(email: string) { + // Simple, practical email check (good enough for most apps) + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +async function hashPassword(password: string) { + return await Bun.password.hash(password); +} + +async function verifyPassword(password: string, hash: string) { + return await Bun.password.verify(password, hash); +} + +// ---- server ---- +Bun.serve({ + port: 4000, + async fetch(req) { + const url = new URL(req.url); + + if (req.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "http://localhost:5173", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET,POST,OPTIONS", + }, + }); + } + + // ---- SIGNUP ---- + if (req.method === "POST" && url.pathname === "/api/auth/signup") { + const body = (await req.json().catch(() => null)) as null | { + email?: string; + password?: string; + }; + + if (!body?.email || !body?.password) { + return badRequest("Email and password are required."); + } + + const email = body.email.trim().toLowerCase(); + const password = body.password; + + if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); + if (password.length < 8) return badRequest("Password must be at least 8 characters."); + + const existing = stmts.findByEmail.get(email) as any; + if (existing) return json({ ok: false, error: "Email already exists." }, 409); + + const passwordHash = await hashPassword(password); + stmts.createUser.run(email, passwordHash); + + return json({ ok: true, message: "Account created." }, 201); + } + + // ---- LOGIN ---- + if (req.method === "POST" && url.pathname === "/api/auth/login") { + const body = (await req.json().catch(() => null)) as null | { + email?: string; + password?: string; + }; + + if (!body?.email || !body?.password) { + return badRequest("Email and password are required."); + } + + const email = body.email.trim().toLowerCase(); + const password = body.password; + + if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); + + const user = stmts.findByEmail.get(email) as any; + if (!user) return json({ ok: false, error: "Invalid email or password." }, 401); + + const valid = await verifyPassword(password, user.password_hash); + if (!valid) return json({ ok: false, error: "Invalid email or password." }, 401); + + return json({ + ok: true, + message: "Logged in!", + user: { id: user.id, email: user.email }, + }); + } + + if (req.method === "GET" && url.pathname === "/api/health") { + return json({ ok: true, status: "up" }); + } + + return json({ ok: false, error: "Not found" }, 404); + }, +}); + +console.log("API running on http://localhost:4000"); diff --git a/user-auth/server/schema.sql b/user-auth/server/schema.sql new file mode 100644 index 0000000..9292381 --- /dev/null +++ b/user-auth/server/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); From 05a344e79529a58da42afba077d55374546d0885 Mon Sep 17 00:00:00 2001 From: Logan Date: Thu, 5 Feb 2026 12:25:06 -0600 Subject: [PATCH 014/204] add database to docker compose --- app/server/db_password.txt | 1 + app/server/db_root_password.txt | 1 + app/server/docker-compose.yml | 24 +++++++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 app/server/db_password.txt create mode 100644 app/server/db_root_password.txt diff --git a/app/server/db_password.txt b/app/server/db_password.txt new file mode 100644 index 0000000..f3097ab --- /dev/null +++ b/app/server/db_password.txt @@ -0,0 +1 @@ +password diff --git a/app/server/db_root_password.txt b/app/server/db_root_password.txt new file mode 100644 index 0000000..ed930fa --- /dev/null +++ b/app/server/db_root_password.txt @@ -0,0 +1 @@ +rootpassword diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 6d429eb..52b6839 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -33,5 +33,27 @@ services: build: ./market env_file: - path: ./default.env + + #connect manually with `mysql -h 127.0.0.1 -P 3306 -u root -p` + database: + image: mysql:8.0-debian + volumes: + - db_data:/var/lib/mysql + secrets: + - db_root_password + - db_password + environment: + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password + - MYSQL_PASSWORD_FILE=/run/secrets/db_password ports: - - "8000:8000" + - "3306:3306" + + +secrets: + db_root_password: + file: db_root_password.txt + db_password: + file: db_password.txt + +volumes: + db_data: From fa7622ae8476130681fb2885feb06c96694d5513 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 6 Feb 2026 14:55:39 -0600 Subject: [PATCH 015/204] db running w/ sample schema --- app/server/api-gateway/bun.lockb | Bin 121512 -> 0 bytes app/server/auth/src/index.ts | 15 --- app/server/db_root_password.txt | 2 +- app/server/docker-compose.yml | 11 +- .../{ => services}/analytics/Dockerfile | 0 .../{ => services}/analytics/requirements.txt | 0 .../{ => services}/analytics/src/main.py | 0 .../{ => services}/api-gateway/.gitignore | 0 .../{ => services}/api-gateway/Dockerfile | 0 .../{ => services}/api-gateway/README.md | 0 .../{ => services}/api-gateway/bun.lock | 0 app/server/services/api-gateway/bun.lockb | Bin 0 -> 121512 bytes .../api-gateway/eslint.config.mts | 0 .../{ => services}/api-gateway/package.json | 10 +- .../{ => services}/api-gateway/src/index.ts | 0 .../{ => services}/api-gateway/tsconfig.json | 0 app/server/{ => services}/auth/.gitignore | 0 app/server/{ => services}/auth/Dockerfile | 0 app/server/{ => services}/auth/README.md | 0 app/server/{ => services}/auth/bun.lock | 0 app/server/services/auth/bun.lockb | Bin 0 -> 123198 bytes app/server/services/auth/eslint.config.ts | 11 ++ app/server/{ => services}/auth/package.json | 10 +- .../services/auth/src/credentialUtils.ts | 17 +++ app/server/services/auth/src/index.ts | 119 ++++++++++++++++++ app/server/{ => services}/auth/tsconfig.json | 0 app/server/services/database/schema.sql | 22 ++++ app/server/{ => services}/market/Dockerfile | 0 .../{ => services}/market/requirements.txt | 0 app/server/{ => services}/market/src/main.py | 0 app/server/{ => services}/user/.gitignore | 0 app/server/{ => services}/user/Dockerfile | 0 app/server/{ => services}/user/README.md | 0 app/server/{ => services}/user/bun.lock | 0 app/server/services/user/bun.lockb | Bin 0 -> 115888 bytes app/server/services/user/eslint.config.ts | 11 ++ app/server/{ => services}/user/package.json | 10 +- app/server/{ => services}/user/src/index.ts | 0 app/server/{ => services}/user/tsconfig.json | 0 39 files changed, 210 insertions(+), 28 deletions(-) delete mode 100755 app/server/api-gateway/bun.lockb delete mode 100644 app/server/auth/src/index.ts rename app/server/{ => services}/analytics/Dockerfile (100%) rename app/server/{ => services}/analytics/requirements.txt (100%) rename app/server/{ => services}/analytics/src/main.py (100%) rename app/server/{ => services}/api-gateway/.gitignore (100%) rename app/server/{ => services}/api-gateway/Dockerfile (100%) rename app/server/{ => services}/api-gateway/README.md (100%) rename app/server/{ => services}/api-gateway/bun.lock (100%) create mode 100755 app/server/services/api-gateway/bun.lockb rename app/server/{ => services}/api-gateway/eslint.config.mts (100%) rename app/server/{ => services}/api-gateway/package.json (90%) rename app/server/{ => services}/api-gateway/src/index.ts (100%) rename app/server/{ => services}/api-gateway/tsconfig.json (100%) rename app/server/{ => services}/auth/.gitignore (100%) rename app/server/{ => services}/auth/Dockerfile (100%) rename app/server/{ => services}/auth/README.md (100%) rename app/server/{ => services}/auth/bun.lock (100%) create mode 100755 app/server/services/auth/bun.lockb create mode 100644 app/server/services/auth/eslint.config.ts rename app/server/{ => services}/auth/package.json (63%) create mode 100644 app/server/services/auth/src/credentialUtils.ts create mode 100644 app/server/services/auth/src/index.ts rename app/server/{ => services}/auth/tsconfig.json (100%) create mode 100644 app/server/services/database/schema.sql rename app/server/{ => services}/market/Dockerfile (100%) rename app/server/{ => services}/market/requirements.txt (100%) rename app/server/{ => services}/market/src/main.py (100%) rename app/server/{ => services}/user/.gitignore (100%) rename app/server/{ => services}/user/Dockerfile (100%) rename app/server/{ => services}/user/README.md (100%) rename app/server/{ => services}/user/bun.lock (100%) create mode 100755 app/server/services/user/bun.lockb create mode 100644 app/server/services/user/eslint.config.ts rename app/server/{ => services}/user/package.json (56%) rename app/server/{ => services}/user/src/index.ts (100%) rename app/server/{ => services}/user/tsconfig.json (100%) diff --git a/app/server/api-gateway/bun.lockb b/app/server/api-gateway/bun.lockb deleted file mode 100755 index f563dc07ff48d1ccd9184c08b0c0427b99975535..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121512 zcmeFacRbhK|3ChUmzkAh&!miuL`GIJvMZZpzO3w(nZ2TdUl?(gd(Zs+15PVfle zwe$C&2A8Otr~Ls(7dL{aqoo9*S)3;5Bs(bI3J}J{9@vNd_V;lSbp?I&^7ja`_jDlm5X(&fLVH?73t*ZbEDC~j(AmY_ z8TjquO9%wr!F>he1bOGcpuxDN1B7-Q2m~)t7hlL-inEJ{gQ&k3t{+SWX#WWjz2KC# z^#hIFT-;s!a8Fp4>IuI7el9=b=Iu}L3BuVs+qxY9T1~7=XFjPnJ6u)imXXb$S(It8?!@t_8shy5Z1f_dlbi`yiy zG>@49!gk~Uq0U!gJ70o_1EAu3{cQdGeF?r(#PW84Funw^<+%9x`lW+B^wZtNognI9 zhpQvj%Lp#n69NeRckys0__+A_IuN`ZpspkE0?r$PpDkRrxSK@Y5rD9tKNpq1kJ}K) zL%+L-m`7~q7vx2N^GR7`$sZrvgQ9kx4naSJm;9mGyu|bO@N)426Voop4~7-z4S#_2 zj#%$0y0pHm0mA;P0EGVb1Ed8=Esn#1DH!oUY$-1T%AtQ>Kprfih_?XYc)TED13(y` zDuA%v&MixRIC(Dh8_ZMBgC02CQ=kLmrVkLt|2*gy%-2XR@oo~)*501r<>!m@-nvxp z=IuK=)m@_E`Bc19@^hPcz^1p z%P+-GVf#`$VWp-1#sP$J1NSXaf~~#tRfVPc4$uzTO#(L<=#MKXhwEknYGW5B4)3f)gR|HPC^&YM>sD^HzW`?!v_Om4FZXK?U?+ z|1PU7*QTOS|WAlx@#502+0z=P|&RdY$_ z2*|_v6b=x^-vc1@V>JjQ%x?ueSbqcLVShzHe_=ft=nwR764b+XA3+|riz4=`mzZw_ z2>mYt2)3|@41h3xQ2?PI2LM7|65zr4Z3pr{;{p(l$5w#Q9v46u=coEhc`#|=cJIpo z$|3I|;KTYFfG{ql0O2}41rX}k0EGS51_;}2AeJ)%g#LX8{eu3C5wQm#)Hw@~17I9L z=yw>HKX5-q`z+WtJlz5axG*q*;ePE25XOT55ZZGD?IHSrN#zGNh&tmXzf4S)`e_d8 zq5qlyp&xPpA%8Qm{JQB<+}D^b&A&3arFtq*59cG^e2M4aY47Lb;z9TT>Y<-60K&L7 z5cA;opMG>&EZKeiR?|Is(*&-;_k3lyvhLG!0uh@vFCQD-ZCoF;Xkik?8bA@+(rzYw?f_?cXys;w)XQ1hxXL?p2TVEZG#!0#KXf*k)%`>{dzvJuO|! z!?Nk3X?%^0tv6c3kE=Qr2uQl!m}%>9cDg5I9dYoLBmLv^gU<@fdGZ#=*3ZuQt;=*5 z;yHmkcC$$zeV#-=f3bjsgnp{iq+y_Wpgf=6e(&Y;6JPB5 zt=CdY<@1?c_!|3T!#f&Yw$)JrlH+@uTYCssT*YcC#Z)B)pIl*4cV)hvAbBcHrRiCv z8SC@86o*^qUw`;ykVdD{r@bRa`5A}yqZZBQ2^$xcEEA~MT2iU{V~g`Q(MB}y6FD3w zq-2*ULq}${j-jMFIK|_X(teWL&!#B{E#motR(>7b=rzq74j-QLKM;u7HKD>xX3nM072QfV;zGxMbC0iP z=-M2+{jaW?>}l7xWhlGX&96Wa8x$Y+$!d4Yq0`WM54>P?O2{v}wrNl7}RxGtS3FU=e*RL z3nf3OzXf;cTuq!LY_t{BoQQckznV^^JXPG{$i|zBa#WrQ&EtErsztS%)xE_$x%+qS z=RfUh22HNx4qMJkknO60RcHKE`)n zkD8`LR-JIcj+DicAtfk2*=o{ReAS7J{q451jdw47_}+ZkLoc&NW|%s;us^vubu|rz z=s~4vLY><&u{BxaEUbgioTQExtKOD%^ZHiD7=PEs^Gvl9n|=GLD&bb;7x!xtvdhYH zhl1&43TJ&7YAZg@?>pvdDQHCG+mhZJiU>AFWmQwO-&l_9bD5o%z+s z;*k{DK&gF)A}R!;T8or!IT$JM&eN{HxlZN$`=jRXG6MxgvvXUdoAhptxU&RCJG!5l zc|gnj#o~SP%doe!_WVB2Z5B3u@k!7kHz!vQQq8KaSErpQQp{nxX)MS``{BYoIoGM| z%r64Qo(su5l4~THF3>{oXEbaSQ)ncN9X2$fIGuW*KYHYXmlqxL zfWp?r(jxK`&$ehJeOKPoTIrmTxl1{TDpC98qq6jCoPAHMV*@&l<>(qbd+OeF@sRRB z&z{lzH-;XinM@(GU2@_1US`kgDAY#hE7N?)4vdVc9Q$%${`BN?^S-aE1DwJ;2Ny+1 z^X}P6#yGp4czWse7Q;z0hL^n#&svicT8dROPX*diH=Ta|a8qc&Hx~BWomA@4tM(># z6>^(tQ`a`;TDFwF7!c7sn#QTK?Zbf=Gd)S)ol-t)$>*v**q+Q*6vy+}Fu|%)#4n1b zh}Zhs4aUm)j`fZP7ZsTLIG-?Gv)Gfk=1so$Eh-KD9sakf6Hj-U;!SAk(Ec{`eq+C| zjA!qJH`}eNZ6*#$Er(WjKN$FCRNwf0R z-pczw27KLluHbz|!m&)sH?kM#PF49IsaEOatm`u@Yr5o_88dc?yY5wW;P{>%jql3u zX;@sHpM2F@cQlZ1@%e*+^O@A!c&d`8LoOO<+ZNt;uF$x6x@EJpHq*1@oi$h5CrOPD z9v(^O{JeT-`?$`DRN8UdLNZ0aE1fvG*?;r zQPO{UCC6y@){NFgs}$3BPc*vEw>dFP-ojhowi&C9CmEo+XkMW{`eJ|O+JUgzr}lD; z&sj3W7$Y2C&r!4n*xW2IbAIQL5~cn|TkI5{(D;t^Vw@V00b@NRJRx%*MeD7vIG$W} zom61Rd~Cn|OT)K&1b6r>=6~%PN*37Fq{yuAqi@R3RCDHB_v4AYPP4q`q~f^9OO1># z@JAH5IKPLd=8d*8?T}XWZ|LZKI)1QcnBAa=b>k@Cz_BcTFO|^zy729ExgP`L%`*8a zOs?EKCP#jb%Lt!uXF28)QDn-*h@ZON=LyEfcsDqC<=w5p#$_r8gOk6@yE)@{C2 z+nNT+ALU&7WQT9j*(sN`Q_lt?YB%VS8-MkR79_1J+qma zGRbDHBkY%HW9`lxZLreJnt(0$%;a)+gvv;mV`2EX#=7aodi zZRA3CbR;5YLnfRaBrv{PzY3hE{!JnwlIsELfO8rpz-=oKM0{W12spwtLjL;#0(cbrIe#D@mH(2AX?Gj&H31)T;RWZ=auTw`0X7b}{t*Xi zEGHqpDd4LS?L!_a|D6VEmks!mfDgwGUMpQc4*-8N;6pv=`%3xqfDhL%w2d^tufI*m ze@(FQ!Tv)6)L3c!k^o=gU+_BsAB`WhzubiUUkzR^KtA;QkNOYzy8s`AaYYH)&m`JM zzOOWXoq!MSdq39?9Q&1y|7!3O3dSGW2U{5q`SH8XApbQ0AG~7wX&-V{>VFL2s}T8r z;{PzXJR|6{=43ixpUSnk;Yb`;si1Clu4gWLGedmqH*B*eD` z{C$8A^UEE3sE_z}03V)TU>RCN@b}*(#HVFgIzPd&hxS*xe;EQkoIlX^pV*HFe6)U% z55Myr)+7IGiT*?1q5b71#3uuXPVj2{KkIL~_YuTb0em?Bpk3g~iqBv17(RF(zU(=G z?AHUn3a}6R4(A>!2fzL@p?0glOM4Z-hyDJO_!$8{ynmzh_e-OgN@V{S;KTSq+smDM zPzLea03Y7}VA)FM0OC`EphG@dx2PQI{VJh$8i0@PU$Fgh`+)dyfDhvb^U!~IEhiy< z1JQqU?nnLjod)95GcB!ul>Z$M)uZ-yfUgStUv9tu&c6isaQ&mTztaAH27F0k{Gc}S z3)%T6MC}B@Ll>6+PwoF+j`$XU562JIgQGBRxoZgVivWKY#{ZR$-yGn>^#kJuHQ_p3 zPD1v#FfYwt$blLw_1_8brGb6e_f6nJy5QGeCe-dU(f)G%Mxuy62KaFOF4ynB^EZMI z7P0Fe)&CBWeFwmY^Y>5vKMnZX03Xf&-`Pd=$bLW3e^|cUwFBECJ_qZPeUxABI!5jH z5c$wH+Jm6}?-Jt213p}TaQx65{v8+ABmNV>R|9-#8whNTs{;R#@5>!S)cy_N!~F;H;I-U-AU+>E4!0lh;r$=X!(}JLrw3myu=p2z7r;0B z7yNp_*Z3EF3Qio(=wI+H0bd31VLX-_dl+OCzihw`e0R2a6501lf5}H4)yi4;J=AjQO-G52>mimwUNBBD*ko_Hi5B-O87usH_{UpFw z1AG*>mGXN4AFf|$ey-V zvQNeTf8W0?iwqhez7CO(=HGJTi1dg06yHm zpc0ixOJMA;lz$lT;rJte5dM?js2sIxAo`E`zf$`o zo0iVMP!FzwmGV^pANmh@=t4gJ3X%VTfDiXiQetCN{wpr5L+#1{UxsM^PtL#ZF#U(R zEA9VI!KL|wcnG1^uM+Y<8t~PK{-d&A@lYLVdk^rj^9PRMauVYI0DL%qQGPjG*ao#1 z7y5VaUu*#%tshFDyVCfj0=^P4e$X#8kKpgWO342Xz=!uwl>Ze6TZh_H3I8?!q3xC0 z-vRh4MEkJsEA4+Ik&k$AA74&F{?`LOj33OSYbAUtk^lSt0r~Me4P;*%@X`1!mkYHK zKLPOJ{6}#|<;%5S2l#ORf$b^4wNgIoW*klhGkz=On*lyNf3KwP&{t$98Svrz7ubKO zfy!ZtmEWii+2<2qdVWQIqjJ>lpAfY>2>8f8)IjC` z#6#t%T@~QN{SR{g#Qzz<*ZmjqR|cEE29b|qfPDNFBL8Fm#$T=m;t|X{0hKF^}p1O~8j-=+jEqKL>btM9+_q zztaBi1!OpW==}7%xS@I!zc|2${=+;fLv4PAsO=!&s}uRKJsO8!abX?eZvcm9bpKro z%2zsmfq)P1pK$HNIkXX6%Sp(7IpD+dJIurR3w>BlLi|NQM)M!~zS8j%lUo}9HGumk z`xgQ54S@e}-U)zfrSb0sHy;zgU#{Q4#LtBKuL-{Vq)gjiu`{;)la|4QSx7IYrYKj=HuTjL<;&&cRF*zJMdP+oz8T=}#*E)`??K2;F5nvx`OxQ;#*cZ&()S;s|ELd8 z=XVL&_W^t~fBuBu1NeI}{a>yIvTvpO*Z4#3O8FUp|8M;72YfjHp4X@L+!8izueei`R0I+eSTf({I3RlW6b)Y zUS$u$4(^Vdr6KidHxJ%9bl`Z);r*!>Ij>vv&5?Ro(p&R>*Y>HOIQCJ#D) z{E7czfDe|y&;4t;wqf6pzx99*_Yau=6Z>phf7$HK8@oyYE<%Z(wjZwtui{PQRAI|uk+3;ECZ(f^{)*eYcIHQ=N8!TOc%AM)Vv ziOye06Km(6en)Nn03U0ArSWeAeC+v?<2+!`nuBj;{*6+7(RRlwVZ_d-v;=G zL_RE|UJe{EV59bfEyVCoA z8{nh+KeV^f{*&+dD}JaCzw#T^A^$A^AI*Q%2h`?Qi1_CLAMSrJ|0nh*03V%SpuOd4 zAp1&yjOC;9U-=H}P`hJ*4CfEBi^`YF?*aV%82(DfZyVTrvHSN*?H>hvOJE<_rTLxX z#Cl}sE#SlRGdU=S!-5q4^FWv`OqjH3({b|5= z1pMXN{X3roJUqek2h?K#*K&1`eNDi}uD_M;U(tXM`%e#S!+ZBi?cWD{c>jXF)BXXU z92{O$i19;n0Lc6{q5fL~KD56QE)+-nOu$zq+J|K;?SC)eqx}QwuJrt~$@H)DJG8yh z^UndmhyKI453iN(-}QhGw(y_x7ij>Q-zF5l4Pf!3_kYWc9TZ1=AHY{5`u`{WFUQ!2 znn?F|9YF1v&Hp<8Li;O?pB>=K0{@}jpY%Tq@Ie=T#vk&RJCBk79|0ds!9O0qT^4`Y zhng$x|9QZd#@Ju!{2u^(xPD>Z;rv@JA2j{BU&4I{?)}WL03?X~;F5zGkgy2*2R<(N znXm|LgFW=;_<_CfXZmjl{Zm=0{)v#c16)vdC$SuhupZ3qp9zbwJvg)c`J79YJOb}r`2=~wQ-w^iC z3S6)qI1~I#5aFD(Tgv`dgzfE#_5Tf_9|U4O7Gb#)u^b}QaVF*=!n_L+U5Vuop`Y%= zat~rT79r1*SPl`+4}W6*R|x$%NNfiY>I4!oh={=eVZtJ87XmKG4+9s>9|9Ll5Me%? zh!Fr`iU1cixDJC0#`h?=V7nA>!2}WNrGg8VA1CJ10Kx|IpQZmDq0T*U zL2MzmgUAW4*TnpPLl~biaKZk}5Zhr9>dg|%A;SD8Vjd#g)4me(|1S{6>8BAQ!gWXm zjKcoI|F1ht5TV};0AbM@V)=hVSj7l_KtEZC$O#bgd0{0H;d~JWc{o0siRHgS*hG}r z4kF}<1B8A`63ZdNU$=rEuwI&oGDMVvl^|gew%blDhY0;vA?AODu*nW$JBUzcCovBZ z`lSI7_FJ1+4iWyUL(D^jJUwC_BFq~AghjiFxCdrH!s1$xcL3$Ey(7SF08ayi>*FFo z@E`6H5ii3GND!ev*8swCFCx|#1BCwGAm+;e!UPevuLKC=at9!6S4YG~V*NdU(2rIk zwiEFIK=2>#A^br^X!jY&Qv&P*2+!GX0YW>I0HMw_K$w1ou*rMy1N!xe*d8MM^)vVZ zb-n_B)8>ngF36y8yxj5!UMvQ6C_z+D*(G z5)uA88BouHn71OL4H4}DLO)#rg8y)C;0KI<5X=w}+6g7*AwoaGh-^vOi5Q3fou7VA zi=XExI7R+^a7*j44p`cC zg?kuE_@h>yPFouf`@H+~MzrtTaBfT`=+dJY0;#H`*Kp8oZ~iDdH%xu$e~~71t!8@l`;OpGcrnePmTf8*wET^j-i8OdW!E91=;Z9s>fy8o{9+?8*9}i zMH?~c(gt_5`svJdtbdaimfbblvV-4zzRE(y@#b86C`K2a#ZkgvVedKBbLvW3$U|?n zfLtCk|HosGE;*=H$CE{Ko#a0}`1!k@$lH+DbXJUZ!8D_L#wW~F*`72S6nvoMKh?!j zQi9Qi&mbt_8#0tc4_;^!ENGfnOVxcg-u_X?^d0Zy{b<%tVSUv~o~+TbVh6a6^qJJs ze?H&onk}Ez&;Rb!J5r{Mju$R)gU?*DH*qwzRfb@6;X4tO@Dc1E zWIA|d^e>4RUgYX{%RTl&@@`#; zp_o^7s=;HnW_&b87rp~Q3ExsXA$H5LY+Ed&0Do7~8s(Ulp1PT56jmE*FFKVT&wKBY z&L7x(g>^KF$8S)WzG>`0uJmCGp?K2|D$n`1v%iDS_GtdXcS|VYM@Ku}|Da)dR71K^ zm7cx+MJ5~5ha1<((jKOrz4bUHQmfmS1huq+?;^zouZ?gyCZ>MwbqW0>mGn zwUI7Jzx+h`$i{WiTZ1m$;S86S>T#HpV&8A@zT1;4yqo*E$Qpx#UGIw&&foibp)O|q zCQFGQ5)0Q-UTe&2VBb4g|H$3&<^W$IItEW$?RQ;#(n$@fSV=qlDjdD|mp(jzmFPYKugp zadpz6MT+$za`shLc-(_CopKGc3VqoP86&1MexqAdI`E2nHph4$y57IDMkGCtj5L7< zqq_!)0>pn(Hag<|?y-86uhDik1%dQz(*};7Q@VHKxAHhW{DO4|&Q*Z#0H zQENTpUPs8yJ~#h%OJLfZ@KLXOJ{Vm_tnPQ==~ulHt)x2+C1_k#c77W-$D#P-y2?2I z&;k814bAeLa+4lmfA2=e2Bs#$k@MTvc)zeZCsBOR+0U}~IOCN(j4ph)g%W-s=Irjt zwOjUA$MmaY-kcCg&bp>Qd6e&sC(}-cam`1U?|j*m@v2P4B@B0t!>@uXxiHPe63?zo zBEZK-6&ovu-6xrlC_ucLn}kIuLyfw%>&KH7?y;=5mF~uzxsUhC)}9)ZH8i4~%G9?d zJh*KdL^srKV|P1WfA8B{UnpmtFC4X0N$%>)#rV4pt2<`yTUFNX;NJSY=z$ThK;SO> zhlS3~t3EmlD7MB*8Nl`*O}<@cj}WK9J*Q3xO>&FA1=P8 zXIb1&mFHb>C`Srl_Z%=3@7CcC2oVCaF@H`9&i((W-g1l9Vg-U8iE?NAj30SjdHb z^u8G<9M{OV*1FTt=0cSpm}Qu^_wKcp-cu2iotnZuT${T`F#d92b*=oK)rkz16gs;! zYN-9#8`^7ze?*h?+MsTYqlbwa%Z-p5t9o{b)e`2nYjJLQsbt^B_qp4IR=rJ{W+$!j zXVo(pUHI7oO8D>!PNt6)THcyhxq@~_ma3h*D?lMydByL6<&plc^%o;YMGmsJcdA*c z1-E=A;}pe3<$4@jXghknyyDmhsc;;8M~n7#E+h&N&lX;KgN`1*c~)6|GpEco2OC<( zSE>@XM%86w+MjZ=%Uz-(m(VE0eLu7?<~f+$u_(WtSxe9S`cc;wy!ol=sRoQLH&*xJ zI?ZE#;S3p{DLY3knRG0#ou4=zMWsppXuBG_-DjUp!Dlmuhr0XFaa)AK=l#=<;H9g=>=(V%Lc%=AGcu#MLk4 zTiiOZZ5+RvML>5)#!NKb8w2a3=Cd;IDcTK=$lf~L6-quVbEedL_2~u@y8Y>1MtT@s zKCJHBnZ9}M_3_jxV}mc&e?QknO7YR~P3+F3a3UhZ!8|SoNA-UI(C&zL&_mm`C za=rKngU{PZ)137F@r-L~hcUYAvAWktP7F(J*>5c%j&tX{TkIl39%`6b!=D)3|FC_* zkzMb+!=dVl^}4eQ+ntpuUZc8uixbT_o#y$M2&gJ!fZX%~` ztQl|X$LMar>gsW|9V&aS7u7+L`|QXb@vqj>-KelUScJU!|Os-12 zTGjT{8CS=OHJ?jQ#(m~KTS4)z>v;V&jIIDy_j2uhJHkX6m1M~p?~kXB@LOsyJ)>lz zoj#`5oF6GhYN|fI>g|trFCFtqSktf7iqrPfS6(dfs>x3gXnj7(EIEYH-H6p?qm40V z-)-74_Ju~lF5jAPVo-Wrrk%!+pMk^3Ey5y0Ma|=TWFM~d#1D;MJ~`h;%C7xP=-8IE zK4TS`oF#0w@ZXL`@!f>gRj#uqxjRh|Kglai*=?ptwMu+CZtc5rrljLqOq~h2cPd8e z9FH7Q%IDF&HSabe@=~)$skINQ(_?r}oW=FuVF-ovQK^P0wdi#KhCN=330Fb6z~U7{7r}ckTV~ zRK?^i6fX_0=q0e$mlof|_$!RnO}SXN^FUfrt$`zhz4EcDLu{cT;p&-e50BQ(r@3?$ z7%L>%R!`^e5UIVY+)%hf?P&|SQT?p&K1vazL%MF^sMrd|- zM`Cn0V|ArZC%P6}i&u5k9j;-L{OWk_Bj>LD3XM*u&qxkj9SRg4+4h;UqP?hGAh1*C zjn-7wC5qRNKTEH>S@NZiCo6kWAEPUZ)lE6rDYp9Q#WU5ep{H6-#OXy@a=aToLoU8n zt18Qdvs1F>9D~pml_ZI%S(me19>%KMB9F(h)tuNOOVGVU9?grLzhYS3be8Zs>!cHd z6pk?s<;Dk`L#*RZ_8gYvcQx)iQO&-$H|q@V$IbVhxS28-BwcwJzGXFq((6QJU2|xV zI61@d9($e?$LiiKZ{W#LwbeI#UxAO~$eHj7eeXYI8C<|0Aa#F4UFfqKSDQ(Kzw#!| z*Y7-c6gFos+OOwYZ8t|s|Lvii+FXSRCJqu<-SFUJpJOCHD@Mey)bezy&h1-$>bxqI z!AqLAZBpdqBODUji*AI` zqH{aE*kA?9~+ZPHJR(457XSp|>jo)!EbS~-n42wv??2UV; zg*&VZM3Oen_mg9EC9%49l0tSX8hvt>?+)J_Q>`OEr&2vlS}Z=4W~pX$km4K78E;{| zweGku{xemJ$*F5BVrtawZ+^(8XivZRCw?9iF_cfWLfFW%v*}!AgE*{$)gz>Rn{~18HKee* z{ubY%^7sg^T_w8 zzIM=z#@Np2;C<)YrI9DDW@wRHuGtk;=ksBehwe=l_?-f@Pr}c%QNq7gI5s?~wMKW~ z$k5FVb|gNV_P2iGVBc(`)AJy5BIxWj=K1FB=BK>L1ve%R2dRcCIwmnn@4h2MQtJ6C z&Drhn(r4*Q?_p$+C_wz9Jo{^Y#x>=V)OSDKz=vB&mfa~lsj5aZ*62L(HGR6+Cz;ZD z6@$W>uN}_@l!W(Q{18({P`R$I+_QJg$7cIQK8(MzSl!~zhoR@MP9L89z(^w4-l}u6 z&Gggj&nnyL*B|(3XLrJ{dNrlwz%_^CZpUA|UNs<_7=2mKKO_8-gjTiIG>1I_yFbWb zbtUepXuMI|DSJe3Xyc%^mTOG4{XHJzuRK;)*r`oC;48;#)7w92yS~opUw07yO2SO_Svt0<{NlpG zowq`HMx>Fqy^Jl+TRkxhtDsb3X-f+a`;yD;BAQh66#E_qeIE_V<2MrSI~9oCmP<EXp&8LKRFzLRrAZSZo8k$#j)BbRIv^l|E;W^n=qRe9c1+T@lmJM(rK?!}jj z>ZM?xyW#iRP{LbtQoi4rV@%0>H8)y(viC`?e0Y5DszW!l^niG(P2$X|FS~z< z>3WdSc(L~Q$t=~KLkw2*A+{f6UVr|~UVK9-)BmXB8i&XXFe$DU;SaoeyH^{dtAw0| zx>}{#xaYL*^g5Fgnj9AuMc-&i9+Ko(&GF#0V1c$IqsaB*{!xK=%k77599on3b`|NR zkzLFxlV=OHG}($W0w;1Xy5P^$|C8`lR>$P*Kc8`%Xm?D$+MJqLY4_>5FH7pn__TOu z9{t2EF)dO<8%~!znW*X{>u+_O#QAz0h;4nKdisFx4I#~gw=udZ|4{}09t%CtyUoR} z+p(bEl}`Sh^=JRIaJnhZ(wk+SO zC#GGZh(rJRa4f4|S&q4GM#Tx`HqM1$O%C|o5u~e%)x98hRz~~GH&Ol@TSRk?DirM; z$j|VS(4tJ^FMLa9J#21${60%%zF8rwp#JgEmUxP652m<+LvQ4-^dvs1t5kc7(cOvF z#h1$HTNzsVPG{T^7!gTsyfupgZaY={Lb|h3m?WzaqUSX<_Sq zagB#_WS+KEJ+)NJkQ+u<4Xdjbsw;Mma5|@%i|ys7RkmvU95xi|t{l2k>JaC1eQ>bA zKGN7kpY==Lg2@36c5 zvGLpG&s!7PxM*KK5Oou?&FN7%h|$%=>f-y$DKfGo8WeZ8+ZQ+mMQ23Wd{tGDmVC6= z)IEva;B-&(7JVPJs{Re6YlBTvEtoI)cY1mqu&t+krNBOVEXWI^y9=v(K6>VO>xt-; z`-Os8Hug0a)Ef9ElPiVe8r`;>rcYCA zk*R2|b;syxVRiG(FRqWhX?%$Cjxgu#j%jz5@$xE7*VSZbvogA-yC}5b~lEr&m<;aU<tuyK8AzT+Oe7g0k$8`Y-im)^V z=Ag)z^_8M-)ju%0==-}+9-r`8`|Sw7CzHp(%fRoB2N~-rB~o>BsRgfCJ^r>m@A0la z63_eFFGSV9e5p7y7i=_B9+lFU?_?Bec+OsvjJMMeqpOGYSM;L&&2?#HDK+=Goz~8H zdFRoz8KsZtwHyk*qOFq750Jd#pmHW4p->L$Bc(+UP&M8DJjh=slvYR z)5q%SMQ5FU@ScNIqvS+@w^5G*_50zoTh0W|=MiE%O9WRvRPr_$%P2V+e~T;l%iCv$ zngOr+`mX4DnR?vSH}%a=RLA&hfYrTjzPYCL0o9Z63u{ilW2JB84-ICCYHmr|+FDX+ zFu7=dGB=}GY4~Q=`nS4%>oxFgloci7(cCy=i>Ti1-1g?rF}l04x~Z0&*Mekag@ZqA z^b6R97c#r`qvhhD9yOUmYkpYD_bzU8sbY~MCanq`A` zeGXw?-o4kv=o(>l*9uV?%t{MOb@{w-iHP70<1Z__>_OP|Dx&Svl=g{R_5x!R(|xLp zC*N$pc5-Wu*InIJPie$xndV&jj_59io*)ls~A}H9u?T>>Fg@F=Yh>_ zn{or%J)Gu!abEP^!Q1*Qt~~0GWW?yA?{h(U zy!sjvrIv4pr$2aRQQNkYaS6LHJ)cNf43IN=EL5{qIct+Onfjo#W2QW<*J$(Vm#K6a zZQu1nU(N6iHfvJl2nbn<_7RaB|D=Fu1>EPsHET~_mpxlTp_OZ@&R>hswZ!TwNK<~| z+F>GJI6+2Q!eA-b?v*8d`drk(lhajV8Dx=!)iOTQoMH!3J8Y=-Sr)c6pPIA}UC5Tn zmXD9}`SSKPaP~j`?!)RDncK1n1ztE-=8@H!kYupa$a?RZXkPXWUJL9@ha?jivZUrN zAN*k)6fmSPxldeJL{(Iy>*HcjokKT|U}u588b%lXodinw{%t4q&Wpdm4@q#%Cwbek zrl*FJ&nXN2NC~QSe=p89O-4~1J3gJX}-nK>!5(PowT$BDQdtfw;O=)~ySVErwN>$#ae<@HXA zY)zg*=H{4_tbVn}X%6)pb?~N$$7r1CTD7a4VXxXZy-Tj4@fDO^@}VL^5q5P{$xqI1 z*nDi$evGayR+r<_Ea8wsGxgScHK9}5+}2&2d%7gYT#A0oEPSpP4e1PPU}V44s6Mnw zyV<{7_0egu4P7T|--m9@CV50=!MbDVyWLCAv36M9js4<0{7Od&tMU@+C-n6A;?7j) zv^=JbRDp|F*y1WVdcqoGVR`@ncNG{6Hk;gMs9k#v!9^zZr48f?USq~ zi=3x5UryOpQf%}-=Q-<1|0Q*8;<@H$JT$nfE&@iEfYl{`EOp{4e}m-noKqz-IzhOL zv#Fckuut9fd1h)iT$lahh3iJ!%&#{$QJ7sm{K?~XQ)#;7x>tujcn75rw8!Hv7hrT9 zvAVstdW@;WdUJm$mh^a=K2}N`kvHBoPMO2qdDmTL$4K+nTv`b~JkC?#u5sR@kpT*g zgqDs%O4EMU@|H(he3w-*x=vW#xb-iq8BMrl--{RMQsBNnJ-D8fB81Ma;&F;fbH_m0 zTb?q}=Td{(!%<>t%1smo>{-*9jh8E?dG-V~yp})mRsy5zjMbeL>OW9_e6*OsRp)Vw zJH6gm>$ct7nmImhd%TXfb*I_z*|oe5w$2(#2jB5hjW^v`t0ga=-y;3o-~PzE`3yr( ztgZ`Im-}H@(^~7@NmnJzm{*l#ylJ~N)8FFm3=S&8?#V0zeSQyLh?w9TNzVt|U|9&> z(mG7DK%SGfCDG~rqy0rEJlQe+x?*)%%gwWm=iaaF88T-f)w7SM@uZ8slp1_7a$nkm zeRd3m>#I1*`aV0`1Zry1I2~f5p&Kbl3h68qWsMXb`(eSHhtWNN)m04TA!inQ!l0^5 zPkL^LlG!$n$*Xhz>pRVID>x4`7WLW-<1yWbiF*C_`4; zE*-A(_uB$LX882Kysa!K+IY+_x%pPsJhO5P?KJQ9d2ip{zM*a2*ynllJ!mM8x2wMv z;vc`ch|Nw+)J%5UCeZ&v7x7JF<;&%8|D$d^}uYuNQ%0sFlmFRbqH?hA)@$%iz3JSl8-Q+~gw zq3|7EHB+g{vXCYKZfM5Z1V#ed<14RFa3K$}uB3)tXMa4R_f>&DQyk)tk-Dl$>RK8dd!2tK|K%$9$Q6 z7NM{1=(${XDb3}ijHG{qiGvSTcgpR@w7ZIjj~AcGsEf#_)rxi58af7_RyC;Hd2a3< z8}j4C`yD%GwuN~Ms5~m4Sf^2>Q@l5eU^7cITlE6p6eNw&^~LJ?zbaQ&wLV(*A>#}S zEAPk9ln@7kYSUqBp~Q1-WpYD@)VF`<4UJ@cFWynQ|C0IcvGmP-E(PWr{hmC=CpXVe zkYRNFu)4y6bd(1Lz4f|fCA~|h*Dors{oMET)m6>JQ3*Mw-H+4Nea^m!C!12&bNnW9 zbWWv;pc@zHH0I^i(j6@$9vZz0qw9~={kS@w>{zj|x2^5tcfBHF>b4>Vl+H4Qg?*}z z)62ZBI1Si44_ECBym^VHM73>?g|tY-w4-&|6^f4+MJSr*i`HUv1F*UbN=XzW{;T#~ zmfl_++c!mi@w{Y6^P*{RolV@#DkoCOjFE}-p6gxoXNsOswNpvf8r?E2ySgFrK&e(U z_1mVD0~p+07S$!yQtl*w1PK(jBXNOAfGTR~#nohX`~!xYAmYmH94mwoe8 zWtLY={Py$sEj?f9OnTJfV%Oe`Kf3$PC`LCBtLvnkMR2(IzC`|E^3HGU!|}1!$0Bdr z7AD?}HyDWm#Gj9C*>(TwHQ_70N5J=L7~bfLmPxLbC|q@Tydl(f z&04w|>z>!gQ@P(Ls}s%(wUorkdwaCp&Aj|VK+jk7wyNI;j4u59QIzo7K{DO3K?nGB z;?4}Fs;O1gq?b!rD?UoG-#a?QDJ9=XlmZ+HLTeleR<16(#A4+o~`niJgg*Zn!Q{fv) zA@N!&7i1_{s~Thq93xEyYz3rk>=jRN#9?&d-?^fMzq_vfoVLL=e5We!?Zyov!Vi+= zjXw`dNh!R_O3@Na?;VpB5hWv)Yn#bgWt3Jo%&+=vy7*JB6yKnN8@mgME%y7Khma^h zd>7U8@Z!5ZQJk()!v>V^D`jV@mFN|lw`o5~Nw1ICX?=>vol@PR-o-CHvuCVcGPt^F z-&4aen^*BxGoLc!*|7Tq{QFy!@N^#}M^7H4;hs9~=lm_!C8$0ataRd?ti07`{d~aMNFZujg zXLYwL@oTSco4as`OM+p4vKmV+?X5KZEZanlXOjzZ zB`wCj2am++=9v2)r)`p=R6XI=Ut3C5Klo_3($?pkdm1u1*;0gx1@=+J>E$Q0o-vNt z?5pHa?XgiN#bP7bf{xivtK zZ)kh&&h7>w|4L6$rj5Car-$?TpHg)-T^#aJa6WSOqQ#XvtJa6a?(Cv+)A8^-w_m5^ z0Bf1Z`9zVuuQC3{V0B&e)iw^%KDQY-wF!6iz+JMh;n|V6SUacP&yF|lD!Ebn+NvU0 ziLR2-jp|spdJGS{MZX)5&58Y`A1;20V6z&-zJG|t>b^92>_@ifzRB@o1QYY?GP9nP zjz$~i!Tb-hHqtJa3#IrgjJ6Ya4n2_D7vd>5$4#vj{C<7&S^e{4LR8n4+jKH9{>EW- zFJ?0;(vfC>OvBH1 zp~=F)+@GfS#T!|P{Nu^GYliK<2kdHi?P(wTpgx)k`@Nt9tnP_#%`S(O?Mx~x6_TlE zAISx9^M;Je<~!xGcs$th_F)>^{nZ-;Tg-ckVotLj-`dx#9Yq${W-7v5=^vlTM}4*) z6Nf~sZgkbXvBfhjvbh_9IoAX1$F3VCCdQq3y?Be{LDyVpLU8vt<1u5be&m~#TN;scl|t=8!nA^k+nG<2Y?E1i zG&z?BSZ?0s>9Hk0C(gGmIkEfA5hnSd*eGF*6Cx9rj8Z##FuF-tUFWEI7VeP)WEO*Z z>=`yr4EpQ0eU;8t`yh6sW$}od`0cMzM+aBaYK^IFbSoq~btXcmir2`xZOzHTP?mmb zLrx`(ZZcMP+lIo+XX%1p6&Do=u*R2W&u`{CL6y@V06`hMI=~bI#of=78BbWldA>4W7pdetnN;&?_8{v*%v%mWBmio9d92CaZvZJ?r$l$ ze?hW7>WRhu4R>#7&;*;g7Pijv5=`d0zGy@S4W63O+wbNiuePHQV9!O zo2gA^LyOq&8>L`%bsBOV@#ZokRRw**25jbp;bDVC&a>`o<0utD|Na#ptGDbIcm*gct%y83?ZoK3|VmQ%*z(b7qB`rFjcePyU{UL3lsb(3GDyUj|YR^o`v zs*DR`rmu_|w~k0*bdO_o<+A0YTJ8?b2o1XCQ&pbS*1nq=qOm}3Yaim5Kq?}^8sQ`m zcAjK%-!xxoN$(c#uzMHgqN>!Z$;!{ueDls<(>x$q=b_ps;AWR*vw*@~exc>AppwYtf9_N%nAf;rSZSvK5P zxLlIRtGQQM&+==Y7~xrc@Vl+W;@5->slJV?-{IVP|5QH4-*l|5ZI+D4gX8pNc5R~g zXKM5pBxk0y_h0cl#{Wc8vE*K~aktgG-ZxKnO}+4uu_0}8VsP`WzH>j7TaVL|`m}0g zS~*5H1FL&Qq1-)TZ|UfJi@n8)9hdtXL%yv}Oe-wx(BRsz^RmjeH{(KCu|)! zrzg|1-y9Sk4fbFUq7IDBt%w$}y+J8|RnJ*F=FMnZ-xWTUWUK6=|Bt;dfv2il<33~v zWlBk4KASor&G0(G*G8c&^l**JuNrMb2ib{qwpb|1vhDs_4Mc=dbIp=NP z_qBWPy}$e2@4LUB-TQyfv7f#E>silw*0Y|y_C9-C3FG5>8lKlw^p3*ltjp!aMM{oO zon9t9)eL5B9gw?!;%L|IpgA^6RxEs(<}k70(@NzV`}7$5HJMb5%_{H6hF4CC?GxR% z6+a)J!t+`bbW2*YJlCz}Q+yTZd1g;Z)gCbqaaf!;*!qgjbd&I>Z#Kzmwx&z_&E2;$ z@;h4>T}jFI+@2@_M=lwdIWsoO6F}&9Fz@EEH$#c%{y2Hi&{;J!G z^nHToW3H%-oVvI=mSmuDP((%f%aD!Erg`}(Lif(+9*amlbFdl$IsfU+D;}EmX5!lP z)$Cd0oFa0v%WBg~?p9j0d_Hz&iMoXZlbcj_Sh6(BCf$ZdQ8DBvGfhk-B-5_qcr)?5 z!9kDrFgH0IXqbPqxRp_0mvN$!_~}MtR(}~6-tMP*5y>a6rt_qCc5vhch=qCR-g3BUGjtTB>Zu_t4J9&dG{gS$ovbKhCh)H@T4 z_dQ7(TNoAKVYtQhRuz+m#9>aYoa1ouew_F0o8ilYEjWF%@w_~a%&kl0M4#u3nD=~W zIhSbCCa{!scx8~gM_MZbw{=g~*^JV$cVgn5-H`{x&h&KjZQ9@9xK%rDL&eRW_2+l) z!112N^EOvD1kn30%M(n!`92~dXGIp>oz9Za_m;;cHC0)CEROA7h%090YiB=F@VR#mwSLfh)FUy}{QsHGfqb~B9Rj_xUsz`C>@{^Yzjvf|K zahVyNNq;dz@MO3m!w-+iD-^c@Aa&5WLz_0`yD-uOV0_SoDT-gS!Zt9mXzRkG9_ zDoB@w`vCO}`2px@9XTz`Jc;)`p$-W&XLr z+a3>lhO5_gURhUrufuzEh~;#t3cpk#9+(kqfLVy!j&Z`WW&HZB7PS@7^tQS}XPF za+B}gGOIS7%|4xg(>EW_%d_-;5$DhF-P*B&)v<>k-hL6K`Cg9IsyeCB`~A!Lce?^_ z6uMmr-F9Q9w6N>il>IUL?`U1PI;wA!+|_V^^{SW=j`sqd_qeIJ^tGe2b)NRbwqM-v z@KxE_)62WB$xS-2zD-RQU3(#J<@as((vCd|t#H+B=zgR{ccxQVf1Amb_=|b0&)z(z z$MIgo^FGSdd}sVAi|0uCo_q1_x}hR<8S6FNo4UPKISuTV#f)yN)d^Xd8XtBerSfaB z{@ZMa?69g`e8+ra7^Awabspl!aRHv!{k7iX@zq6KPmF~28VqJ|xQAR@cXD3jI*;h} zGqSv7HZ@c^vis2WeVcW{eMfzL=c>jwt>K%7imNmgJK{IAq~P=QOL*Qlb)9`;hPOxg zQ@5uy+brF2PD#h?wO&SC+=t^ob8^4usD^)h%*NnO&lUM_@#@a=t_@;GB$>?1GMuxH z*PM_uJdLwMA)a@r_x%q>&X}qNT(xiV(~Ng%C4O6J!kbL~_#o?O)h^|WYGEo?M!sBH zLtdU^Yp%u1KUm;-^{wO2-IH(ZEb49)_e-Ppx%pqHpor(AY<^#_4+n&pW8( zX&UKP_;Z#`^44`T4nL03d8B$PcYyH+9mmpFYU-6y7Zb1EKj?UGT~WNIT2!K^Yx7L5 zapzlowsW&cxz#OcINqyx-so{hq4S~z+(xJRM!qC(jyON~themOLe+Y=+g2>49xEfR z(66_c&mDE#)H@T#u?|776lmT)r}=H_z@JgYoLljyD(X80xrQG_N_L>MUQ8A^j7l zZxNoC;qgiH;`YaHIGjrCcIw(c(UdG3Uh_aXD06uGL|bmvkTczNgBq#Q&<(K-Gp?r4 zU$x2MB1Puv@ou~n5bN;e8U8xXbv*CUmi>DVUDr9rrNy_%Q#)q;j6+TVruY1}ahuK| zADlH;OVZKsbkl}|wWIuvBe6Bl%N(A{sU~|gg)Q%GyvJD<@ExabF`hR$^862Z*WCvu zDsM&J+@CTl}2*9scxH4mTPux^LxqDJ8Hskay!}AvA z%q_KiQt|AiT3lGq!R&O&Nr8CQxBE9VojuLbxOj58Rr-mDliphBkFuig6U z`R&W=)U^#&d3W#dwZie#Tr;jdAzP#h;_Pq>&l~veoJjsPv0eV{88TuQ%Fms9e_$TNnIZF1 zc7aFYTJ&@~x+-3XchDD9Z|glNw!Sqtw!G;%|LJn66WQ7_IhW;8dQ`M7(%$9;8GyY#$7 zNy`AGB3}G_T#4tsRNY#;fmy_Fm{--puA|Q^BhAZE`djtSCjt9&=G<&LFQTfN-gI6x z^cWXM)B=u^F+;3rJjLy5hP_NNzBg`$;m7f9Jny2*mMpPyUtVUvv<=dp-CpkUz9M1Y zkj+QYJcas<5+#w!PwOLMb&9KAI4_uev-tZi32vw5xrz7uJUjbuzptHzzd!X3o>weC zY1PC!jz-DZtovElY4wQLHAj=gvaI&pk*^p#5+PK({q9#*Z{Hom5mDx+6K)joRoh9} z|IpiU*XpPEI$K>eM1($8fSS^lUudc@P?p}MAMab_EgNI(;! zeedFVYigr6F>Ybi8oi$TaM{Gu(>GcjJ?m{w&poy#!L4-WfzCt0<4b%ibp@E|T!oY7 z+MdgNn5QS)Z1`v>_j8lC*<~Rd?>#)P_OpU>Yw}Di?fhdFSDR=F*WD4;Z+$tJRl$;; zZHKFAZOzgQ*>+*gf(w^fY<>0=2TX4D?(N)Nc8N&4@X08U)&vBKVg}CX#38N`T9H@I?36qf0`!!eD2nHaNVZ% zBbkgJ4&&#c8a!{k-)+e`ldJnS8{FU?{_fxAsQ&I?$s@V;e$tsw&UU%FpLB}5{L-FZ ziDAEV@`m%V+|$*i+=@D>GJ7T;=*fv(sKDv_0M9EC)4$mu@6Jxa;n^`#8;njoRQxb| z6WJwUab0%kR#v@B&fiiJ@3zUu@f&EDo%ECsRSVt7!91pYh@E_keE437C62ci&#Ra| zgZZwl+UEKfJLi`BEv}j+UZrQon8_c0C^InFuw&i@DUFqZ!nW46XPDDDFU%KMljkJ5 zi>=qA`FyjhYC=^qj<*ia>wCfP#cOTT_yIO9y__9UqDMH&FV73Nb=zEK5__2I?Z@Ey zwVM{!A1-L=Q}f??zj(V8&)fpblJ@E^ZaE{@*9hIi@z&#cn>epAZn9oC?wi-9vU_(( z@JDtIT`@63Mk&?J@oin6=}P9R`|opd?2;SQc#<8jr@qOMVf&V!pGH}Ce>nVE$^f5N zH{f~G&u`|rKkLphffv0Fhk2BH=zZ$OR;F&adHXlE2(P=QMuKsvXP#}68V|CTG!C5h}A2%vyHQ?;<1kd|( zaLZgJL1C4-hy1%TbIUfp&+$~3pEYY?i?^qQU{Y|F#)ElsYk%WD#L=Z3Uz zy?@|I#*$T{x&CX~@qYFc&wI)yv`M)`ML%moTJ5Z_(#e&BD-TE7uwFXHL2isVv{^`u}P;Hwp!q51M-R8p{<)<6vuVnGuaX)Q#J@zs^Z)c%$zIn`CozKiU zpI4bN2gQ93;w=pIeARNy><&JjwcvU0{|s-87CT@kv-&}#^obM6>%~XTZe6;F<;eOaK2j&r-S=6V$V6|gFtEL{Wf}hdhi7g zJW?uPzKUPpd5-60dc4@Mc7gV)&1$?kGW24$Ci9>4sNC``ib#H2I>LTEWF4QL(M!u8 zYB^n3jPLH53=s@VEvoBV$h^p=##e*I4sYKVc-~_vJC659Hn|_O(wxixVXV?DcK^@t z4U(IMV@tn>l;l))ZXc9ZQ=yC8HT$kkW}3cgcHmOu>T#KLi*L@g(VUaJaQ)Vb=e=F9 zJIkZ`=#lw((pd|1-^G<&d~Md@=I<u9-F#F8fd?=yb?>uT3LKwe}tDKc9`k5`oUXGa*_ch^`a-#fd)w3?)x`gC4# z=i-(9INnY?uUKqz?W)`UY;9k1RG9SQ>~un3`4(Ee+c!_&p6TPZJ^CcKW{d2|UUtWi z4kL*N=RPW!T%)n3m%~B{9uafCw37|T+lA*%{U+^^DW@ZJz_?MFWrmn4W63_|1xB6o zmK%K$Ug5t@sOqAVrmA%7QgYOi09(s>Bi-zaOIvnHERduBa<3{T6rcBX<9WqSY_;mY zc_&9fZ)U%g|D42~6W0TacIGxM)hjeA5*^lCvt_lFOA5DEQb=0U4F~#-PhD>{?>%T; zB{=H6r1?Z7{`|NH&l@`=w7cwmu=~$iy14rsk&H{WhyOGm=UD!l&K(&RnFvZ~brh*X6}-9d%bbvYGqv$+G0ad5x)ssa{u<8{HZ-oxYYy;dtNR zd8-4v*l)JUb6;atGF>M0%szOd$&$B~)usCEq(PGh-*oSh24mO$>`;?)&sx`&`_^3lTeTG4nP}`i()Ntk;>WDrFI&#^A9#AZ>a>=B$DOMUM~!vvm&)Dr z?XJi1_ThQgZq-c*xA`t|=gH`sMeTfin__%|tmbHaQeznQS8k5z7gF)f^WL6wNIUW4 ziaE+x$88s^RQbMFS>}|dx5M{?106WtemrkWVwqCy@nuE18jpqc-5I$rx>0#jy=je5 zkfG9f*|gMM^Vdp0xX^n(#AAi0WY@&Z3PBU;ia|I2eAT;8ya`>hs##) zdn|dXXZbeYwedX<*zTv!*!r4Iqijpsj?*C(!ua#<_jumuy9z_hUQh%SKDDc<3wGG>0+sqs6N= zr*9@G?OkfPYq{vm`StE~#~eG2dV$AouWuvstD zePKN>(<^O*gZ8mW{I@P>%BoK=2DCdA+%%)x7#q>q)*!UIU8@g>%Kn zK5$*z&5q+8!t*M2G8pI_l`UPnqxZg;^tk(x!HyP*R||U-n%3%cS#B^`?-5?UZSgQm z`lsj~t5S*NgHqHmPcn|eH58pzVG18 zSkJmrBfs=}$)LIU`D7_t3y)i#e>280bZ_MY@*{64QzT*wOJm&K1p>39ii28Ay z9lqju)0pPp9#P5*dG<y@JKHf>R~V%ZsW-n(d;SDkr+a2gHp1UfXJ&4HB)+ zIxhQe?v+Yr^O8`5x4A(hjQDlW5j^iE-AzjDAO;3OEY?o zla5{!NDCJ=_xWLX^_i`V9M7toSuNzvK^quMvdnx&6IU2VYz}aCG&#U*aj92Gpi~gx2sou7N6D5lVV-LMn;42hn2+iv*Gkv%A?5XJSIeY-GDa9`_^+VKKO zp%Oikt`x&SWn{kWc@@D`FSjhp_~tcwzRKRx>%^xNr_AspNo!QOwKH(M<9Oa~=3uj9 z2elprZp;kl*|Ou@#<>+;Mk;LT@)vp*)&yA{ zKQ(xd#a$(x13w>s$Mdc^y0d(xiU3Qae_i9x(lYnzm5M=Mc?7n;IpW4s5h81?`!ilk z;Al;R*+PDI9X4|5+MMmD3(Mm6y%#yZ{SZgH15V!`c;46y^{SEJYVjuNXlLJrD+ZD{ zE7&4~E|1=9we?guSEm^JZpCiZ$ND$7+w=8%aQe7rBoy+M+TC58S}fJ|R5ufUeSQMZ zOSi`U&4iGZ&K2MJ-?y-ADOc$UG?$1}TEm<4)%RyvN~`0J9?_Qk#N5-;<%yql&+N(m z@S4w=-Jk6Hsru53%q{qQauUz$cOYFs#Darm!E%Siy00AL1+$ng<*0b9JJH-r@5NLT zxc=wS;8=z0>lZ1-3~0s+dX+UaJ}+(&h%L;5cR;9KXV48q zoxOV|ul3Gq=|i>Gnf;ZHdum7OS7<753h|};H9#6GCFfk99w4gYem+kLWDmx9a7y03}r86*@b z46D?0c+KUSm-M;j%aA~H_;HWwu~5S$)i|s1 z@NCIR%W7-BEq*^bQkJg&G0~*b`18Ig!=OV~g-i{7qKWQg>%HOeS+@(US&A4jj;imzUJ!L=cyo$i?f zBlAz^FJW9a+}|@Z4}ENW=O3lIW{poDj+Y6~%YL}lwOQF>g;RA}wszkumiqbImxYP& zl|FJ7eahM_P$>0QF8}MRn6Whh8a&|~&Ua>N3q0x7SlC~{s5YZF+#3J;LS{T~Rr9R# zt}N9)#``y+^c-;ZS}{!}z@|Ka%e;-%>RS zy0SQ}f>Sg?ucC{0=ktr>^+I#sbnp53&X_)sOZ7hctSU1%)17A9qRI3l))Jrf@6~L? z>C1}eWt8IBvfDK66JLYZ!RqEIhy3rQ1y;(G?={*&pQjx%WaeJX6HkR zK)-?~*Y7Ysb`D-3w(D)tR+EiuBZYL!_11N6Ft!Vu7xVJzo7mER@yTxwBFdf}O#Em! z+vV=%AMD!ryaf3MZDU|)rMJya-Z8AYcV}*P#OQo3ja}?b<4N^a`BxK22ishiI!5|_ z)m^+|)UxKl=a1JUE(nm>v=2Nf2s2s}Rumh6zkdhimb8sQ+)HCZLxOqch-SBU$NI1>3X8v9UgN@Pnf+PE#ia1t zlL4U#Z{AYLx@$^aZ1ZJ{h4}Qv6LGwpcwWZE&E?X^7JbZn$+>3x1DSQ{59Vx7I(4h+ zzJgdL)Ay!jxdx+Y%=5%!=}wKF_dVfUA$#K9Hvus{qkJ8nhy8)|`13+8Jg-Sn7T>0u zg<%r66iN-PPinu*{Fr1@E6w}S`q{I@A%;baU!U5nKDv>f!y#?dyzX^V+Io#GwI8m( zYY?jCzMCxSiPM)G&zspk@09P&J zu2r}5JG~DcZZz|zYZULd_&6jhyKS<(sZ!!8ju-s~jJ7ds8ec5Uen#Mo^!j~kHY=Sy zl@a^y#`tK*z>Xv-?QSNQSLB|B^aZa|R+%O~>8VuQ!#`tH`EflC$IN#HqILP@{;zPn zJa}GlSs(RH44lJz>JG_$^OWamm)%*hg0v`l`=QrwqWNuD&0kJL7QPcMJTWk65_7LS z-?abhZke)l))41~4+Tj-@b}@N+?}>DWNNCiGOcBKd3JNGmM&)UwOB^wINkNckwhUCgYwQ!k%G$}jVo}xe0bh{ z{u%u_E?!2q+h(tLot9quEW`2a6FzsT=WJZ7wQnWQ;*Az96E)jW z=CwR^nW)WaZA+0)*KoY(9wFMsa3n*qX~$>7>H}{s7Omi%!T73SK|s=OI^(y8?}s+A zo#m`vuVYZ!{p_k=c%!h=HB%iMrh&GMdpn0;#@WCBWT$cw$BWKkXdA=lk6oc&n;6uM zpUi1WSjiS06~)hYap1|`p6@R-mkTr93`yP`InwoRZpQY_QnP0B^vA`WIN~T;GyFvU z@UvY`ic4_3i}1YU6Vk(;YhMpWYB21V(ED-5)-arGwt;kMkt<_Ks!_t{yLp?U2U5B( zjJo;PEq1(aYba9nsW<(h{fbR#qD#$3yl}kyc-~8N6$dXm1TP(6?Mi(UE%wk;<++9k zIZ!@s>&z?KJS#-=cs;X%Uk&YGaUEXb+H8M)&;IzrxF0^!AqSH=PI4wApQXjg#dzL; zrXQts4AD#I;&zDcSUzbK9w9grU100GS`1Mp|6WYcw&mnv!^SSy^)+J#IYlp|?re@9U8cO^p;u`ju5{k+Atwn3NzVe?9LI_9 za@SrQFS;j$wlOTxx6|HxqW$JEuI@yJ_qsVt_R|%Mt$wU3D#3I3!U{>35l%^t9#dY; zfq|^H`5BMsW8K5dAG(h0tvsusR@Npog5zC^=Y5rW`0ya#j*C(uVlkZ3(#5BDMmRP& z%e-ib@!auKe*hHanBe0b$|4owlSQ` z@EThD@(kKVYHR74J#TytwQ|(WE6jg$JKyr*u|SipXO28rk$~e}hUaCmXz$z9*?g)|C2?K# zVB|ZM$%{)Pb)Jl0OsaHBcq1`tc-fwZVNQW;!{U*HC7B0o`@+(LIFlcT?=d=jTg)Nm z0ge}4%b;xxZ8ut$JD2heRPKy7ZgqGip^{L)t~4)j4ZV5br)`6u?3SFiD|BZ{kGJ2_ zw}4qR?E_~E1N)xD)UBaC%_Svkl2F@e<6=3UH>4u;46pI_uYr=eZ`nR_j_4N4Mb0Ub z_Y3NI|1ECJ`*EI0)oHIc`38DPRo>T9SS*0vtr z?(kUyjbQm7ZO4DQfBzTxP+zw3VDGQq`xZVqr~C4Mu^azqbw>W@Lw55gdy`0Y{Mh?= z_x{f|W?J!S3rt&J+5*!Sn6|+Gum!O56enLN)PzCh>74L?xGSespSHlX1*R?VTMJBe z4uaE;DBEj<5URzfuNoSwmsy;3JzhHrXVM+b}LhaL( z(-xSvz_bOXEii3?X$wqSVA=xH7MQlcv<0RuFl~Wp3rt&J+5*!Sn6|*Q1*R=9ZGmYE zOj}^u0@D_lw!pLnrY$gSfoTg&TVUD((-xSvz_bOXEii3?X$wqSVA=xH7MQlcv<0Ru zFl~Wp3rt&J+5*!Sn6|*Q1*R=9ZGmYE{QqQujnoGjwoyM>)qK2dC7s-Te63wwC0#vi zUF@A)$&#ktWU_{Uth9iS({8edy|{q9fVHcWgS&@4>#x>K{-kVZ4|=X8`b{JJrWD&~ zf1}@;(%x~7o>7VZo&gZYjv|rJb0E<^^t?a?>OQ0z;$j4Z1CX5Z&Ln{{eTZW2O7s^zXG_gbPF4d{CQu$7MbiV-i|RqV%z&8yc-9&TH4}c1i?s)ySBRb? zN8$ifLqYUz$xT4A8DIi11#AJB0k#6n0owrE0TuxC9{HUB^!}J#04snszy_cLSOd@n zBm-{>AQf-|a1xLPI0ZNj$OL2okUdw!@!EiS@SPXH2UrML1b|08kro340ZRcw0Aau~ zfCykYKoqb7uo8g!LINNOkOD{pWB{@NIRNSxMSv0jz5nt7pca7M0a*`d1fcf{qIUqI z_w}K7>oozI0qFg5&j8N>F97I$Xz1N#ZGd(FdVd#s*H$N>3(yVd0h9pB0Of$2fa75I zWIzfa6>tJ@5|9Qs1-K2U0%QTQ0XcwkfLy?NKpr3;Z~<@;kN`LY&;ssI_zVLC0d@m| z0sephKp?;v>}&+k1E6<07yxvjTm&En5DTybSOKg7!GI9Jc7O%I58w|#5Bu-{cmlit z@Hj3K`suYVz!~5Ia0RFV$N)RQW`GG`3%~%N3DAQ2(KE<30V)9WjQ3&KuLPgU05mAj zdjOaLBk<=p0D8a27eE!D8gLhI3s3<#3&-UEase}-y^H{R`0fC31h@cP0qFV8=-JUe z06%~~0KJ1D46qll4-gKB07L?!0WpC6fOxH37`~E1_*+EcL1;8 zdl#S^&uWTIkRKs`LSt$M0O^bT4b|WXp!piI5t5^_C>|i+;{mJ$pm>3FMLHuJ zsslIysD51l@=N5mssL7iGC&ES0FVbr1LOb#fQ0~Fz&rqot0=zC0 zVVL0)>4kVvOh!7Px=}pl0?Yw$1Lgwe1JH5E&V1C*Mew;8zzE7DyLfcy;k=z4%LU;_Ygp%`ESFa;oAMt+UjVGFPZSORteP|Vo@*bdkR zK>dl1L;KM_^fv-(3)+XiTLE?fkiBi-6WP=WfcQ|qIRa4MH~hBmmHuKM05bTmU2i;sH^BeSk#3A;4ZhC?Et74A>1o z<Bbo_C^2|xzm6d)gv1~>`G1EBq<0Z4}S|9M|7>^l!Y z$K?Rd0@4B5fGj{J;0yrC)2W~5;1kJd`w>Sv^>0)cO_u`rei4B5E(D-9BHLUAp!Qz@ z6az{Ds84SLDgZYCw*bfoZUV}w-_hTd)bDq2`4BgpFElL0H^2zsD_|J#8PE=R3U~m(x2F#NM!F&$YXL|< zq!a2_)IQXfhk!?b3_v;n?W45`9YeDN+V=#2?9>K8Tup$NfL6c@0BTn=03CzmNO$xd zaijL5?=)SIozUO4Q+y(R)F()PbUd;Zs=EV#_S5J0w7M*Pl)#&U;xk$ z=mj7hkj>u!knWuTWV6=*)HmILE&$>{oahsw4}kb<0kk%u{pj!a0Q4O%M>?W?NY6n4 zYAdokvNQ4()Yqt9T3^xHiF^pP6}1zMLA(uUGQ^A8jgF()0@(mxC;o3#C(T}HKdK*< zM?Q)^k!~oyp!(3?=os{gY((Qkwm|(!+lTnjF{sU`e|`dJK7`tf>Of^sJCP65cu+aS zkGPNwwFLnkOKT79Z*(l;rnMIxgKRheKt6`*NBe34s13B^(0C*Ij>bIAW;IZT#=%G} zgFf-P(vCqoqq>kjNOxqrNdRg$vK`8E=K#0?NEcrCL@^KLz4HNhPQ-=Rf%Z4jnPwjp z@6h`=P&`1f41YX&p9ZP}&x!P;>4?T4iYb4VXTUxb$I{^wwPmWO>Gb5lU2<^R&l4FGAwE+{;@6~ z^J`IKHXO?eQ}H~;ZT!ak5f5h4W5wZryi`do(~6txr1uTaq<6LUCHweN@&2lG0d)I0q!%Ln7@fl{{YlVsmiyu(5XaAxYjUk<^wIeez4j8QqGbDKvGlxaA3hkOG%I@ODf2N*5ObOA4u$9XkOVOq5S5T z1Rg~1140t-g7Rh--3U5rJ-|kLuS2Qn-r!4|xuf)d*o_?M2@f~(AxRkC)Af?{Sd3{P zD=9uNiTV(Y4r3L8=5=kgM`^QIKXqopf)`n zRp}|Trb%o-V*x1htEDsvn(2i>a-P4vrum94Xh4(vxjme8YvEX=)1P~RlWrSGkd3XK z{gH7~&bQZw%420gA|ow{`UkDQd4ptN^SuYp$vMs-p|~5A@$w^k z2a#IjzV^+&DoRFWB%!|%o1d=}_*>x0&V?bB)$Skx2SxSRJGt9Q_<54p-#W1`DB9$M zv0=J7`A8r)B?UyzT0xs;Gic+h|J>4ovxm8Fq z4rS2D67V-H^yV?Fgfh~SAcWind(jH)6q9{awphxK%z*| zdI41;#WB-nNz&B;j7F^|%%~C#OZxJrk27C^M24cZ zEl4eqb2%^H*nP-71uKKPl(Y{doKS|mr$4}ZTh}?P43vVi z4yr`f_3D?}Mn?ybC__C^CIckgP{zl|Ws8+gcoayGgFu<9AejS_-h!Z{t=(SVKq3zT z2_*MG!UK{HK|XF)b#{8JjI0cZTB(xZR-w5W=X*4%Y+!>ysw5%f%9e@H#w{2drUCs7 z?Cf!Gz?O=3i9@b|zK^#)1q<1|*A-*5zCB3+OpMVG?K& zoZnIFR@Dl0QdF9=eri2*Ngn4dg&K=!L(I z?R*AoHDWKZ?{qU#Y+chYf5ES?<2`7Ad{jn(Xas}S#G{0V89VFSyRL1YkUI020kDB- zB$ePi2PDXMwp9!=rgeM}#afCL}Z1*^EBfA&y96^XdaA)rER_gC!N8_;FX0c_Oh;zAC>0_kMm z(&2Bub5k*i0vg-)Fy;D^?MRQ)Uo{n6Uy=tBh?SD7V7&bRcIVY~TkI#p{>j`oI{gcorfV_6r_XoT^L z)F$?wN5F>0_J)g(13oIWy%k4KPV86hJZl-7En%GPL*6y!dv= zt}OjE8o`ijplQYhra@FD<)~J5Q;GR}kf00&;=vT7ks6Wy%*IJq04xiD#n0PG0zH#- z`Ru*-c8DE_#&nZKYy=S&S}F&F2gSD+u_BWi7O^Z)hU(Y)AVI8=noSE0AJyoC1X%~# z=z!~S%F%cxYxhNKW!uHlC_=v=9tR9(FVC>c06dPFI z5xlFP!uJ&<5OolnAxMzNyQm+H^?KTNA0*Uyn^;0H2g3*s0VhCgtf#tKBbj^Ts9J*! z0vw%e9l`VK-4ecKZTPH*NiaX~wDxtBfZ_GTr(&{svl_Av@*U6(84bl9t+}sKR4QhM zQp z&B3U*Aya)aj}j@&Hq{whb6!KefN~08Yo|)|L*``ISYmT9H6nRRz!g!*_MZuFdso@e@9^D?x%r3Hay}IAK5$N$Q+N<=ZG5WKq;oC^KbmW0F!x zZBZFxsXLi`wN*S&2E_v?Qwx<*&gB-&B{T_f9#3q8zv@OiXZouKGzoD%#1i6Y`Bx2y+oPc_4<~{0Bzrq2 zZy2Ve27Tq8!pA*EU~5InS;6x~lrgu(>iy60L~C>ghCCkn^v@C=@N0pP5CKd1DKZqQ zR}p6@Q_PJhdjbz3_5*4pOzbZ|Aoi*-C?f!6+!ueT zXnrod>Q|Y+W{`h>b{GylfHGp*SvBV$=iC2^ztMX8-_iO%d$vpLiNv}Qx0Kiqi0dKt zga1}br#zQINdSr1cmA`wZ372|RAle_$fsj$>{{1h1Vbc;sX<{%Tg#yt&>700sC=z| zvC&ZARg}9x8A#zs#My=~ltKA{z_wZ+9#;!hC_|kABdBce+^SES_in1A#t>y#Ve+=N zCHug&7M|x!WmUaPK!WDRDWVHVWFbcayD32!N4lwhsk~gvso)3_G|zz~6eMT{WH6UC zZD7hnXW~c$kTik>WezK)*8Ui^;Q5S|!Dhf?5c<&!cy9j(vWT@P%K4}>dL~tpdHE;n z_uVb)KmzkDnrSbAgcHgvTsq^-rgV9j8)*`#2AiE}>wNTdD`5}XSYZ(f&1NhfYWD^< zoq+YXJ)kwD6(lccKiYT$BkbKyeQ5wTSQA1MogLZJ4oTz_3bo6XUC|l}H6JDWTB9W) z(hUbb+d8^ZlvBu{)jYVu>28Y>E>caQc4|tXH7h2;W=$WmJI1Ei(5G^)pl)ZH2WB4?kb@W;vI!v=1+~wV^x_a&=@;Yg=2gr>_sGsgik3?^p?1k)g8r z26>WEHt4hK2Y=#dELt%`5@5qlWc<8cy*Vq2KacH51PNq`$amllHD5m;vQO8-JHwGK zbSO`xmN^K$fc)TEZuf2ur2@3RL#^jHNRU@a(H%*)tg}WE>bXohRpM5iwMVn#`6g-^ zI1|qU3ChL9Yn$I+d^ATFlVE4NMO4WH&DQ!`u_v#A1o;l=R!NntPraqo!x~>eWdj@3 zQ63xycU=QTdQdS3G$tVR$r1LE#_^cvccTgO_exXNNvCQlmVT!p;ZE48>C8@ z`ULCOv(CDVmBDlyr%FzSh%>&a`i52ssJ+0njN;cP%f*T{f-bB4Dl-oxT%dt*tN*yW z=Pl%KRJ$z)$sCZh>#S~T-!y(6D}#*^Vp|hi=Qn*imG^=Uggy$k{&zGW9$Ca5Ptd2} zqXb?BBa3+5h1d^>`;@@rp&l|>!rsZ-$CvX^P)z*J4-hTU&<52=Zcc7w2|F87)!pn* z6Zg)d^v1D)mY+E7k7gj&067Zd;AVIx-?fCgc zZBKSZ{gM#t)(&N6Lz(xN)H8UBwuoY7u=NmPt%-H}Z)xD*LCI<+G&emcaZE;Qw`dgy zdK=a$JObR|PB+asmi>mEk3fQ=3yKubQZ#S>wSP(_DH2al_IVv@a+6fMfvxX=1kKLO zX-V}$5pOdw3Fe~>AVGFJnOeKR%dLcsDuH@hL4tBvNwq3+-vJXdssz~jsgmQ%YRG=v zCRM-KMnS?2W#*p=PCb}wid}`HWGJjs*frt3g0nwmjxq74U}gTMJ;b^Z+dv2OLvi%V zC8i@yGJ0{;dSLIA?ZJGMKsP8uTn};25o=9Q57>H2@5RO(u?EC`T?CBCLxP7}R)ytz znxiWmm{XB%Q6;_kZ%o{;r1GE{5IX_;O|0^Q$c#p#H>1njKCQ%2szt#^2_iWrA>ROdwN2?UY+*h;qnmw z@6aBYjnEbh86ooP&HGyl7MmNQ+#hCUv`v-aQzXoAwuiiTLzWs(+oC669hi2ojSnPq zKvE*+8$Rojz%F0|OQKC2Bmy82(@!|MXq_U;uc>Ex7eRu0f$viElE)^oJAbhepBY_; zGH9HR7^viGH>k${Ds!8vflKR`=W!Dbhki+j*@z_#kg=ndp1;ana=US<{XfIz4Ckn* z7tU|rw4duuzSlofhFF7Zt0-)9=bsXvN8g|Uc0)5D)NuzS^P!$(Ax-gCcXd9HP*=8F zK?2Lfdq1qUFjv^X11n4*!On6%QOn%AF#PtF+n5O^p{&q)*!p@qxswlUS)nKKsA>=- z)UyH>6-uAZHEZY*7pk_T)&mYS<%%;l8i}K_IItmqGkv*RRBVwlSacEW;e7evjtV}p!EZ|%7jjYX>ZFDl{wqEu+I;!Z+lW%rdXv`q^vB2!V!GHmU+87 zNaSe(?QRAcS<-Qkpk8?I#aDK&*31sm02{%7UFW+9$3kMeS4@?XAFQobpJ(iCycH{h zUB^%Z3G(>3nmugK8aiMELwjIFMu8L!666PK?*3S%S?6_tS`Uo7(;!(0lA*nI8&`^q zouEo!e3yb`9!NY=Bm1%!SEBPdbgl{B-Ut#j8Z|9H`;Y6KpB%INiNVb`i~cp&l!cAWxM2d7a;w@jw~XI$%-aS<@NHpx(}8Pbu{~ zcteX?26{UjB&g^37fpmE<@LhVa?l!^&WN)b;&B=WY-pT{Z7w~tQDnCP)drw-3P_OM z+)k7WCRyLQ0}`+rT6I4M66B+7FNTkqUuT2JhiL#_bp<3S+u$^+H&gQ#-H%DItmYO- zkngl@RlblQQHp#A;P!K$G**?}Bbte2HmBd$R49c%=H#E0w)cV1idRm9MDu>30X z5ZI8d=SS6~-ZDRrt|lSB28+H139=4PQ@qZtoVpK~1oMy&AVJY3uh~5BUR)u%3Ws_d zt|om43F@C0yvdT>d+lG-B)`k-h_VfN5~DiBIzl|RPMvx7`28<7PLLp5D;n(|Myp1%1?dz@lqFf_mYK7!i zhN-esHGoKd2_y?ZGGC-uup{w=5LE(W?g21@VsZihD%flwC5#AkOn-f6}0{C zG`{>L`An7U%dX;XWjLrtm4F|xXi(S~xqVC0PwLTPBG`H%NRV}gX7M^ZJ{ez+mBDiV zl^{VDHNUXqv!LtdCaMJPgZ$6NQF{+>39>a@WMA4J6?f~=ai|AI0vcJw(S)m^$-~GGkWqRKXtYt`9QTSFP*=udnmL9D@)cEd&%afBsqDY1lDYhn$4Xi>((jlh9T z&-EtIJ##3Efkg@CU>FM}t0~civ})KtQ$H@4S_bBYDSI39>nbRN#_8OIK*7lu4INZl zLyojkn=`eRGnBxo$Kb*m1nJCa0M+rXmo8jx^;WX6_&18n ztQ_1PNbEaP=A+mMCWuI29pV_zsY{u&YSvUV79Y|u#zqOI8$oZwC?UvU!3M;>;|}$p zu^s&Fv&8LPIdE=<3K(-B(n)OHm&RX3O$38ENi zPzoBLi1aik!*ruuG9@F%*obCyMWmaXwI}=vWm~asZ>wbqwI1+M;;fTPkCG>**E(>1 zC}Y4@J)ot~3k#`|$(@I`zI&qg>=zqB&p|x|9s)f_T!ugbv*rVRitjkSvpW>7`vzsv zD8EM5k)*`tQpX_}ekMi!+vWZ=>X}e=`IAXWK&%#ULLg zXb-fMIEGw>_MoiBdoq8#r8|BZ^a46JgEGX?pEwtrGDrGd?gG6{eD*;+7XF>=_Fs(g z*xX2b=1LU(6-Wdz9?m2PR<^+g#C`hT(OUjDzC%2h5ZOSUs68;35PKr=%tUM*0*{9g z{NJi)N{`3ZEr~TCnzt2xbA^LA_b2Wj;*}EOGQ^{v_LY5yG$ zA)d{M&rALt4T$}KxTVB1#+3Oe7OS2bQ*xw2wpO>msO#toE}BnaED-xBu?EER0fIz(|)jwp`)cCII`hu9OR^eSxLCT=OQ z?+|N1oH^iapWVg0gNnS4G7kiAtn;Af$#jwY@oq^mW|+w z4QNd~8i~D%c%CC}DY5SmtnR~^1hEYW%D}l9aeIi_{`HnB{-#ffBNDLfR_v*spwJ+Rw zduZ-X>(VmH8a~{X3|Dc@%qZuYeYpoXT3VPq>FM-f4=3pKpY;&p-X^H$x4AOW$ddg{ z21!s4@mvBUi`e6*Y!7B@g8KqsmEhu5%DJYqqyA_>rz+*DBJ=^+8?K5>ah(Y?nDT15 z47#hIxIM&L6WL9H#AQxd_upzkx9CPkaRk+SA;J=SBC!SpG32)vomw~K*u=J;GE!hy ziiuaYiSPaT@3cqeH!+d;n)ttXB?Ys=|NE=h#IcuQ8)fxtwIscxtHZZNn7_ewQdl2_ zvA_lK=dV{uiSzg=J>++*x-fnS&M_d463->X^8)enHVC}x-->T?zwsU75lo-~M85xQ zq#&s0-)gDsZ(2&++r&0#-cFh4g5Ntd#B#q|itZbfLpP3~-;rYXM*Q_xKC#qi2-W=Y zEFt_elPPx|N$Li|uVCD*T}{wUS`HclGQS=(r2OkKLnL1xTc`(qG4h?~toPAG9vdfj zDYTEHBTH?N->b~m$A|3gPxjvVRAAp``*ouZbd*^>Qdyy%E|7h4l&S+~D+pkN=YzsC zjbx^Jq!A{!_H>ePfL|~NSO-b?Xi@HKl5+OZLidOJNZI(gQ@^>x@8Bs?2~Sr)2Pb#9 zwF7bm>@ZtzCr@AMap*}v)UVVB{80YFj*+19{{zt!a_k04Z%1EWPYF+NkH8=aHzzwg zSJ2U${HsDL89K^GMM?_pG;#9r^$wDB_jGgik@WC(kowE9QWAgNCqen;q@=II?kP0E z9*#4`{(qqMh3E00U-iOm`q)ol(T|*^sXww6P{V#iPU%KU*Zor(|D&o3=z%y$MZ`m~ zo}{zSZ-}UdmY^OiL3!8?VsLYEcXG4#wRQCH^mX!ZxAyh$w)OV#fg5r>tpnWAV=N@V z{A71uCpSND_z$LaN19(i=hB?O$JWD>?CN0$Qb{+mw*y(r(a+7=-QGtEc?!j1P&qP< zy|s_81or!WvX`H=D^%&{ZS5*y>*0nfqH$9+_O?Zj7ngwQy@Mod;ZBM_2nQeV51=Ib*jmFc%Rns{gOr<- zJpWYIk?iV81|O!@0$RHHxjXp=AqNAGrX7ydrwXV?Qd|YI!XK5odD!_ubDZ2fJ-lJ~ zJNtOJL*>>su#cjHlYNjxfRnEy{6-EQY2o1xfB&Xe|EcoeIsepg64xI8NmO94q!2t?Su@`Daz54r_Y4gjk2@J@7{ek)K-*KSl*+41u@#}ba44|u@ z9a+NF2W2A?;H9=Mzv;7I2LaQsKQO+(8-Ty6Lc0G^FZ9*lxBt%*Ixzfs7vx~nAb=j^ zMVa@1(~@5YA+A44V4>viEQt<>CxuZ9!Z#RQ=-==A5IuPeil7gu1#Z$s$75m9#?RiK zjHRjsNht0pV8Hza761KUp*au5`Mhm`g7)>7;*|740{f{lN-I3ER07LTAd&iAnmEPL zP{R~^u^5Ee7G08>f%AgqSD~#E{Hx%9mP!|6u zW++T|N@6QP$yuns6#=t(xcx>U{9!wBbU+I!9yj}mJ$&tNe4hG5J1F#jnmgO%Ms6GoUnLiilbuwu zN!9M&MYh^wC9>qG<(Y9#zYkuLO|ZK~^vE(H+VDuA5&ZFGrx#m)wd~#; zx?T4y#>ceANgrnBeTN?{j8=&#^ZrN8VNr{(F$wBga-C5kUh5*RFj!NhhtW8iUhac9 z1?WIFCiKVN@(LeRC5QyfChF78)|rscQs{hUeD+bAl`I%(NvnSQTSV$t#8y1(P-M;X zFERtC1+%THJCbkm0b|(>Ca;P*lzw>*p^qYYp0N!okS!i9^;LDPyA@@|&axZE>+tkW z7cj}B&d_10W)fhbP!ErqVk$|bxl}4*^Ju>joht)c7c)7g$-Kj}^NB$1VvGqa8ltk( zW64%-3Tf6*M*$>3-UtsL6ay^4wZhY?~lR##<*$^1CE)iFI&&IppVCB4Xr6Z7^W zl^DL^7w(x=E*E$%rk)y&Ev`7IT}-1xL|q;bwSx0oqO~TS`se3G6PZ0UTFt-@?tx!+ zy!Zazn^jycKwM017O|aov7pQ$X;w7*=U3=OrT6I3s_dldR8?=}L!-YmN%n=93j`c}8=y7bm!ZALKL1V%+$YV_w4{NZ|573vb)q$iGFNL>u-JcEgSDsjf7 zy!9kv)hzi;GCr=B(D0X%&->S&9@I<|kWtVw@x+*Hqh@Dku`dGZBF$~N+>YlDn*ezx zXx!OeRCmK=r9khbS^3P?S=th~hs*v_a`LgOc6{ndY6X2pOkHOwuBM&#TtRO6HX$|@ z`F>n&>*Y52ZWJ$cc=OS1n7*-9x!U&VZ)}xUwK3{RhZwu?M`@aRJ7Uq8KeHqEDGwl1 zSe-c<$?WA^i0lu;yiGm*L1XgeLNtY$={UF*yL*Ev4@fD% zsK;x~0UIkSnpX$OlL=ZAcWSUgD(~aH9+}n`7^C~oUB}uAF1{7ESm7lF91|TaSixyn zFhSX7CoXZZ^xden)~QGc#2(3m(ec7BvzNJ=4VOs zcG`1+V?(BZNl6gw>~bt0rTBWP`cOh(Q5i1z&c`q=F_Qyy@puvUc8Fg9zrMDLq&lkHj+ z0e~7YlLMce4MeuyxeZvg?3w?3<zVcQtQ0m+vLb(Z z_=n0gM(()q8xDYJ;$Xzf)TTlpG;!&0v@^KqP7B$gRU1KU9Spga!|4afOz7|D!}003 zleN|nhR+6wkD6keOQb&+=3_x7h4c$xC{5l)8qRiBgV%laN~t(W=YRHx!`0rgw4*7E z@OdNo<@S1#{tVYE$BsB#!D8@;^WlKecSD6#8PfKr7knEhvRRMKAUcd=r!j8Fr}TR9 zZe)D4(jPZ@~;$i!_-5k?7~u6jSzY zbFDcX#fKNX=)mw%_=vXz?NMsc@e5;Hwh+r>vPP3%Xsu>XM^}AwL+hiP%5@qAxry?-1+vRwmgB_ob-KoZB1K3AlEZ)^?3k9J~P$NclE3X$rU4p8W_HaARVDGR)V}F!C zDo5L|5a{nF3VU@L0{cTv^4!)-AShWzCj2zG8f_V9H}R=Kmr)tQ5~Rd< z*W8c1(E2Fl#vP#yn5J^j`>1LhS*~uzfZW9>Jr8oM63<`wsh(TUjj(MT z$2~_GcHxNzpABRmRkKE8RC@Rs$`nvjMl#Sd(vavcwa-q#PG|Yc+Uu{;_HFY}Ju+l* zN@jmP4>B9>SbIL3RnJk?=E5A!Mn^g$c07-7W{>*JC-Xb|1kO}7@vp=F&;B{9wd`}A zGK$0~(c94gUj4>0`#f-&i=(t~$1i`~H*lL`;Amb1NrI0om3-hW3j>rk!Ng~WEc@!- zFbzf%2O>L5Em;$id11$o*T>6ge11PanQ6Ae6@u}W*OxoaIKPf8Fkh-L_?-yMJ4Pg| z!7GdV>+K_n&2|%bY4`N8%SSRhV!J~D&v(AmHtL`@l>n=Wn+Q|q?I29b3H21NlIv5> zq>>F5aA^)b0ipy6!HAr;{XwHg;<~nphS0myt>bK1xVq+f=i!>oc zZH^?l^I>Z~AZkQ4&=Qx&UbqW#VD5dhT>W!sQy##kFnq-4C#GVcoWk-kM>WX5Yy({m ziFZ+*htHSW(XL%|+PY=}qDD4JFa6#QbSXf|Sy#+^gQJaYYVC0r7gFz>Z<`U!HUXz& z9*sM7sl`HHBl3Dga!d>SGF>|5*{kPJ*dOE-kCyh^N#wUbRh#ApgeM8|J_=ijC%W=v1i5XPhRJV=p?yXqD_wso8n7=6T_#FO4|oa5i#I z@oW1Qh8KLfh?X5F>_+wvF`4B&P9C2HDb*_8pv5=*L%E8XfxpTPGxH z``ex?k^{g0q#Ft@Ug2knm*;2vYw*keJM==0fOCozjD@$WoijRq8Onmhab!nE%rZ{v zu*q@GQY>|b+Y4@a)@h~(w$_?*&`WbVzCnEUc0+nMp0JRRt<@;APt>Yr^(8p*O&IG4 z4?Yvo-^2An96WZ1=ESKke0IVjW^3o`@SJBJ|h!X zpOo=f}&p+U)5-q|4UP`9)qdf01T$!^5XE|DhXB;X25sEiNaxvlR8$$R5L* z*E{WymmqdM548t|6e?5H5K#!7a@6#sXudxB;cJjuf-NklFy|baLXBXTj zqQv_jRg=ipoAo#VXDJSae(-qY9fsSOIu?DEVQ(6wJMFrfpR!idRibPkv^^DQfy?DI z?8^ZT64IfA>~JHVCV~zVQyvPQ!nLpt{Xc6Tv@W)Z+IP1!)c~?g z1k2X@7`#l;C)NxPm>8N#E$`*IQ?qpfN?HG)lDMcwQMnVmA5R=B)WFawW|^5HmWjY; z#X{oy@#Fk7jq@Zh91peQXp`*tHzv*Hj8^JN6Eb}s&e=9EGejze=qYhc>s3ji)t)8 zv1Ax`FWk2a4VJa+t{9I(8FnhNub+!$Fkp)`Aw`)YbXXc?iT$)MO2++yV0nDsdCm{S_#f z_dg0d^4+bH=d)rmv|MkoRWhH1E8kwH!>;1qV;4k*gs7qJEanBey37YQKI+l9K)`C^ zn-FX+l#OJZl(2*p*2vr%)r(>DZOV3(G*Kq!NWt-Gs!61@vdI_D7vR=EfSKRruxgh< zPorZ`Gv;}-*c$px28nI4nga=d5|qHAVXSdD8L@VE9^CFubgGweZ^h{a)cOZ;6^&q! zHjcG~)+B8;x#VszPh`TFqT*wNnU@qNc`*}&Pti%JXq2x`NC0YNlO)#NnudewEd8Be z#@&48Pz7w30?L-HgSn$N0h#_g7nuy4sPorW(cCi4f~)D0Tcp=B%4r^3O&4OM3y!kP zVe_O-I8vW=r#b@cd=+}sa~kDz6Lo>k4sGv|-Nf}`4l8>l0N?$lP4@K4S9cJ6<;JPu z3>M(XT)r7&H_GyZHy!ev1Nt1&$naC%<1tf7rv`L%lyODxSEMI+rt7ynDV29LKVErc z;?H!&wo_(6X>}==g4eMn^Qc#}+ydGrSkH<#25aR3s1Y#`4ejQom6dDLN{#H&&t02n zPnXMR?`3}JzZ_W}b_b>ecgA~s1XT`}0@MC9<`U{3;XI1iW zJFQG{d1pdni?f04F|$rB*Rlh?mJFBqJhyHCy&PY6?`OO9Xd7-lZ}>`XN?1vi7^41` z#?G>_U09uVx^?b15+!Aqowc%$oj97ON>McDlrL4X9ic8W1HVjEeIvW%z*7xubla-G z1UJpr^A=>b*B*Y6SgpuI`6PZe&0UVz^ecJH<|XDYpvKn#7PR?*!V0(L?1I`Rl3qD` i@@|J5d8t~=sJB|g*1SE { - console.log(`Authentication Service running on port ${PORT}`); -}); -process.on("SIGTERM", () => server.close()); - -//test endpoint -app.get('/health', (req: express.Request, res: express.Response) => { - res.send('ok'); -}); diff --git a/app/server/db_root_password.txt b/app/server/db_root_password.txt index ed930fa..7898192 100644 --- a/app/server/db_root_password.txt +++ b/app/server/db_root_password.txt @@ -1 +1 @@ -rootpassword +a diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 52b6839..5b1aa57 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -1,7 +1,7 @@ services: api-gateway: - build: ./api-gateway + build: ./services/api-gateway ports: - "3000:8000" env_file: @@ -15,22 +15,22 @@ services: - market auth: - build: ./auth + build: ./services/auth env_file: - path: ./default.env user: - build: ./user + build: ./services/user env_file: - path: ./default.env analytics: - build: ./analytics + build: ./services/analytics env_file: - path: ./default.env market: - build: ./market + build: ./services/market env_file: - path: ./default.env @@ -39,6 +39,7 @@ services: image: mysql:8.0-debian volumes: - db_data:/var/lib/mysql + - ./services/database:/docker-entrypoint-initdb.d secrets: - db_root_password - db_password diff --git a/app/server/analytics/Dockerfile b/app/server/services/analytics/Dockerfile similarity index 100% rename from app/server/analytics/Dockerfile rename to app/server/services/analytics/Dockerfile diff --git a/app/server/analytics/requirements.txt b/app/server/services/analytics/requirements.txt similarity index 100% rename from app/server/analytics/requirements.txt rename to app/server/services/analytics/requirements.txt diff --git a/app/server/analytics/src/main.py b/app/server/services/analytics/src/main.py similarity index 100% rename from app/server/analytics/src/main.py rename to app/server/services/analytics/src/main.py diff --git a/app/server/api-gateway/.gitignore b/app/server/services/api-gateway/.gitignore similarity index 100% rename from app/server/api-gateway/.gitignore rename to app/server/services/api-gateway/.gitignore diff --git a/app/server/api-gateway/Dockerfile b/app/server/services/api-gateway/Dockerfile similarity index 100% rename from app/server/api-gateway/Dockerfile rename to app/server/services/api-gateway/Dockerfile diff --git a/app/server/api-gateway/README.md b/app/server/services/api-gateway/README.md similarity index 100% rename from app/server/api-gateway/README.md rename to app/server/services/api-gateway/README.md diff --git a/app/server/api-gateway/bun.lock b/app/server/services/api-gateway/bun.lock similarity index 100% rename from app/server/api-gateway/bun.lock rename to app/server/services/api-gateway/bun.lock diff --git a/app/server/services/api-gateway/bun.lockb b/app/server/services/api-gateway/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..86441aba1c4bd6be1a42adb80407cd27aeab9172 GIT binary patch literal 121512 zcmeFaby!u~+CIF1MM_JD5~75JSTu-qsx*jnN|%(zpIF-~YZdu06Tei2Ht?@r;;b&b2&;n^V}^(?i(O!A01{#fQVv z+l35Vf=;ehr)?daYy@pxJ)AAP1bsxv@G%&S;LXREX|>a=bL*NPWawV+)aY6zO?6;v z=g5!|S1iN#V;aN*tuPqg4Zx86%3=P(U?k6jYw#;ifx!e9gHQATr9lN5z%YQM04+T% ztZY0nm=#>P9B_jYlwSr2>xDo$6+j11K|3c`OU!YC4gM}%`%fTG4eH+kgmzxw(Avh* z+YZ=t2HG%f2kmJ>>`AUGWT`oYVn*tEF^R#iX zhB}^J7GB<-HlDG#a(jSK&jy6j!Nb#QKgh%WIXgJp2wGcWOmX!uKxd#%J3!b!2N!!A z4+k$#Ya6#;y0*Xzd!TRQWdYsBoWSw;0m3-AS$KHbczZYnf&4FjaHx!H=jG>SBk16X z83L8i9}kN&f|jnz<9iDfG6Hl2_$%I^68d)qtx z`is~Z1npq_oNRnRG%%QK;0)|vG7h^yJqTg2f^s{hw!dQ6N{hC78Ksofk4=e0syL>H#7J7n_BXCx{bR zA0V&9y-{yr<>~<)v~c`o7Q?j*gJA)DN`SE6nZQ2mr?-cLpd*-DZr(0_R<71IMY!@y z0HM859O~`f$oqgi^v4m0JbO0!$p#R{PY0C4d8C3vX&jpIZrG~=2zA{oyzB)*P|nyp zSi$w=;A!Iv2Yx?j597Jcw-Lv00HLn6jg1>zH-P)A-qyjzTF~1KBPFn5Zyyfb;Ff9O z1sXd!I6HV@_U+xMxAFA$a`+V|cW)ao)vW9-oK6ERZo!T51@l`FjFlY|$V2}>0tB{# zC4@HYfpGp>fn^{M%dY}1B|r;d42A>XB0xBf9|1!CC_w197OotB-^RR=0(m%pu7Nz{ zCjuSVJ`^BaPrd+Q|06{)7%)`9!}~E9Fyz7a0D>tH903qa;ovNQV2TC%0fgh>3J~&; z{o2;r0jxF*=3nQvr;W1@T!$bmzx>t!2-l76;f=g1KrrQlU4VUP2bpJ%4qgsW@2~j= zK5qy0$r2m;jFjX?Klo)f`Vk3`3hKIg2-;X!+24}dsP6^sU|eElaq}LOLpw7d59ehq zsE6x!O>Sepvf^}Fk8G?{U3uI*litt=_ZtrzI~(8kKnLn7fO@$8_5+0Rnc&?5_amFRUj7{ek|?f_ljN3i7aBD6U_x zarsVw(En0^a6P90gz*an2>mz>5b|OH56)9dkpC4IfN(ta1BCY20m3*x*V@Q~%^faB zPijyOd5-}f);9ozaj5_Z=UFO1sACQg_FoMkY_|(nP74tF_XG3``ZI~c0f12FDnM3% zkpQ9JXTkh|^ADMq;P&e3-*)*Z_p~Y(aa79$->=feoQaZ^JKr{f&Ma zfqLk_GC=5uI6%nXiz~lvun~6}!;Se@CB9Kl3hLo}#u{z#tX-|VJRDqXzJPk@=O92B z*A`qJeE#J}pYev>_fyKwaeHPkKZs3YiCG*LKRUN%7)~62q-Q?!b0ywlv59VgsIGNl zr}TVNc^CCTd#s_TZVyr61AbwZweKc$Upb7rJn5WFB+rD~jtgmfA0*3qHyBPlw!dm> zhsI~$%4SwBf@{aK4}7>@{*!Fg|E2n^m|2_M7QD(A!!QTlv2?ZHsA}uG(|<{2Cj9yG zHcGj>3Btw~cHfZ}Cv}x-pFW;mFR0e8=q}{CWBBNiq2c2z1zLkXD=uyg{2BKIs%8Aj zB{Bl{5`Ji1dUvR2QsP$0Rc12IKHTL-L(MK$eZiSSMUlQ{@aPnMmHBV?~sZ5=eVK?oH6yKy#of7P8;>SucAMldAI z(H&*-X{o*zWNn$R9`6bCS*_I#4k)G0|E|xX@^t5y%1()W>bfi;Z;Czd=@pgWQ+=5z z@k=lmbzgcH{o1QstBd4}USr`E;@)CrB7HB8QnB?~5Tw$gT1b_QLRw=3omb*h~2>b19f$Y&OO=AA9o z)9g1dO789~T%Y7xSn{GzbLQJ&S(s$0ZF6qwjEB*?TiUmuo|)}uK6vp}EY)&uF+M)k zr#}6bk@k_hMO0QNZf4JXw;VRxPI91#)A0IA#LrzH$vK#}h4P3_pJ?wKu*q{2YN!#C z7v+7H$EfJYa4%XkHA$}RMU5fTtEEfUb=mK~eA7;%lp9hz5-$6KRqaWK@~i0G>oO+M zq|6-&q{9(qMSCcM+fNFdkK~iFj5$n6Xi86AUhjX&B~|7W{=FA-ByWtPIQ_-%EIxR) zZQL>-QLx!%S^T@E;|bP`v|SCFiMJL`OD9FUKOcH%(ZcyaIhFaFFcpdA&VYSoSdRWpszf8nS+XeucG>K>m1mxf>wPDrNV}acF6C-}Rm$IMFsMU2 z-Ou5pR^~yc*5q6J?J%K)YEcvAM1gXp^_N%NJv`wmGja@sM(kSbVV#uY4wNi+j(aKx zZqKni_3oDb@kd$~)Kw4rxuu9B{GuYinI4P#;n~G6d`qTjJi_v zQdIKV^p1syXfT`{P;9lb>e(~zkvL>uM`8H7OG~)rI&)Vmou&IOX}N7wVowbVpQtWe z`@qO@ua8tQ4DUqD%aR?2YGjSA`6e9|gChdU7n9i34}Cd3I6n~k!|u|eszkngk7OKk zY2?nQI?<*z0$!oyr5t7jm9#a@y~6*f+3CJfXU^)q!hYG_h8|88^O9^2j`k$$V*m)hBO4=G*A!-NXEcU0=_* zGuPef(zlN7IJd38XJpl|_~chkrxP!9tSDa!WS*+Va7{(THatn*_G;mZ;oWWSvZucK ztQ@^o{JAa(!$~LpoJ$H(b-gCvWU%dROl|eSARahwNu^Mo0T+ zE1L8deL2@(^^9bvksaDu8#fn_tF2~H(rsU@l$+VH_n;cxi@2i=d5>lZ^v;|gPi9-( zHYz!-o|r%}ZBarf?UmPu5nu4Rt{fXcNb6OTouQPDd2+1nhw|81heBg-^(7gt=Xp80 z$M&amuA5#m`1nkzKfBA0dbSR0cF%mWF$#Z#G}owFaboaP&GwPAjnA#bX2+;=H zzF#8l^fA9vY-s<{`ckOk2Q{HoPQK|QTtaM0AwH7>_&WoZz6v&*<=I}ryG_6|YBYID z>#fezao!^y>qRRsN8@-@+N2q@JhTkB=^C=G^*@~{>@zHEk1dM~xzS2Hh`k`i&h{fH zp>U#;?#MxT@0Q-z&!^7}jIn5!GVPw=9Enfoc9RP%Y6_C1%>U{eWthfUt)F)%UYzL4 zU4av|*N*M>S9Rf?yZQF}h{hY2_ZcGG!~DL20v`kUSthxIl=$XnIOxi@4LbWq+lB3VU_bOq4Bn*82U_VswB8rCjnfYk-d7U|2H~!{aY!pB$RO(|Sa&)|DScH9)95 z{5km1G5+1l=?V|l#AP43pTFKb>bLv(#e>Ma|td#z5L?ZwSpy~6r7SRJ1^0jTGZC1bL02B2d5kn6`0IFPxbO8k(&UcTjEI_zfYA3 z<@glwk1=6^ggW7cBDcrJq2VJC@yNLMitBZsz z1kBj=MALrc!UN~2f0FQV_t@b|!@WCTgFyw*fk1g}p0bdQr z2hUmXnQ&7H>Awo-xIB)(DI%^B!atAWBk|kpI3oORKokY`As=2_jsH3zfp-&s#eb`O zelT&tBloZPgXdiMth=d%`0on%Vt~&KSV-As1dz6sfPVz=Vcfv;?xqvMUjTeKe*yfm zzt#EIPlUmMNBUp=hsL+c=K+qx`G@$k*$yD>T>)POXCIa!<-gNG+EoL-9L_#0+p7Od zfG>iwzg7Mou;Ih;NBlu>v;La_KJ*{j{S*JM0X}#|^~*k7KU?*G80SCaAs6`g+l2I! z9=t4q`yaCIkn-ONAnlF={$X7I!93n{Lio{ukMtjsx7z==fG>x$|0jG7@N!83$47jE zI=@RuTWi2a&mTC3n@I>i6Y$~uMe>{B!Zt|zZor58FOuIXe;sEZ#vM$hEyrI7+_2#I zA>)S3qu=>~`0E7t$oc7a9CSUx&jWlIe`p`t-m3i(z?Z|t4{9L&03ZKrLhSDVFG=N4 z{fGPbW)i|b1^CGPhh<3l?=+Bhd4Lb^A4q8_hH^d$fc=-zBkHj4*->m

p$!RQvTn#unuYW4e;Uld$VyvbP+xuc-aisAMF31 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 COby2X literal 0 HcmV?d00001 diff --git a/app/server/api-gateway/eslint.config.mts b/app/server/services/api-gateway/eslint.config.mts similarity index 100% rename from app/server/api-gateway/eslint.config.mts rename to app/server/services/api-gateway/eslint.config.mts diff --git a/app/server/api-gateway/package.json b/app/server/services/api-gateway/package.json similarity index 90% rename from app/server/api-gateway/package.json rename to app/server/services/api-gateway/package.json index b4ce046..d646ccb 100644 --- a/app/server/api-gateway/package.json +++ b/app/server/services/api-gateway/package.json @@ -8,21 +8,21 @@ "dependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@typescript-eslint/eslint-plugin": "^8.54.0", - "@typescript-eslint/parser": "^8.54.0", "cors": "^2.8.6", "eslint": "^9.39.2", "express": "^5.2.1", - "http-proxy-middleware": "^3.0.5", - "typescript-eslint": "^8.54.0" + "http-proxy-middleware": "^3.0.5" }, "devDependencies": { "@eslint/js": "^9.39.2", "@types/bun": "latest", "@types/node": "^25.2.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", "eslint-plugin-react": "^7.37.5", "globals": "^17.3.0", "jiti": "^2.6.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "typescript-eslint": "^8.54.0" } } diff --git a/app/server/api-gateway/src/index.ts b/app/server/services/api-gateway/src/index.ts similarity index 100% rename from app/server/api-gateway/src/index.ts rename to app/server/services/api-gateway/src/index.ts diff --git a/app/server/api-gateway/tsconfig.json b/app/server/services/api-gateway/tsconfig.json similarity index 100% rename from app/server/api-gateway/tsconfig.json rename to app/server/services/api-gateway/tsconfig.json diff --git a/app/server/auth/.gitignore b/app/server/services/auth/.gitignore similarity index 100% rename from app/server/auth/.gitignore rename to app/server/services/auth/.gitignore diff --git a/app/server/auth/Dockerfile b/app/server/services/auth/Dockerfile similarity index 100% rename from app/server/auth/Dockerfile rename to app/server/services/auth/Dockerfile diff --git a/app/server/auth/README.md b/app/server/services/auth/README.md similarity index 100% rename from app/server/auth/README.md rename to app/server/services/auth/README.md diff --git a/app/server/auth/bun.lock b/app/server/services/auth/bun.lock similarity index 100% rename from app/server/auth/bun.lock rename to app/server/services/auth/bun.lock diff --git a/app/server/services/auth/bun.lockb b/app/server/services/auth/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..d4d299274cf2d6b565cf0a5d51e3bb15101121f6 GIT binary patch literal 123198 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%0ATG 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≦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 null)) as null | { + email?: string; + password?: string; + }; + + if (!body?.email || !body?.password) { + return badRequest("Email and password are required."); + } + + const email = body.email.trim().toLowerCase(); + const password = body.password; + + if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); + + const user = stmts.findByEmail.get(email) as any; + if (!user) return json({ ok: false, error: "Invalid email or password." }, 401); + + const valid = await verifyPassword(password, user.password_hash); + if (!valid) return json({ ok: false, error: "Invalid email or password." }, 401); + + return json({ + ok: true, + message: "Logged in!", + user: { id: user.id, email: user.email }, + }); + } + + if (req.method === "GET" && url.pathname === "/api/health") { + return json({ ok: true, status: "up" }); + } + + return json({ ok: false, error: "Not found" }, 404); + }, +}); + +console.log(`Auth service running on ${PORT}`); diff --git a/app/server/auth/tsconfig.json b/app/server/services/auth/tsconfig.json similarity index 100% rename from app/server/auth/tsconfig.json rename to app/server/services/auth/tsconfig.json diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql new file mode 100644 index 0000000..affa45b --- /dev/null +++ b/app/server/services/database/schema.sql @@ -0,0 +1,22 @@ +CREATE DATABASE finus; + +CREATE TABLE finus.account ( + id INTEGER NOT NULL AUTO_INCREMENT, + username VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + age INTEGER NOT NULL, + created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE finus.credentials ( + account_id INTEGER NOT NULL, + pw_hash BLOB(256) NOT NULL, + salt CHAR(8) NOT NULL, + PRIMARY KEY (account_id), + FOREIGN KEY (account_id) REFERENCES finus.account(id) ON DELETE CASCADE +); + + diff --git a/app/server/market/Dockerfile b/app/server/services/market/Dockerfile similarity index 100% rename from app/server/market/Dockerfile rename to app/server/services/market/Dockerfile diff --git a/app/server/market/requirements.txt b/app/server/services/market/requirements.txt similarity index 100% rename from app/server/market/requirements.txt rename to app/server/services/market/requirements.txt diff --git a/app/server/market/src/main.py b/app/server/services/market/src/main.py similarity index 100% rename from app/server/market/src/main.py rename to app/server/services/market/src/main.py diff --git a/app/server/user/.gitignore b/app/server/services/user/.gitignore similarity index 100% rename from app/server/user/.gitignore rename to app/server/services/user/.gitignore diff --git a/app/server/user/Dockerfile b/app/server/services/user/Dockerfile similarity index 100% rename from app/server/user/Dockerfile rename to app/server/services/user/Dockerfile diff --git a/app/server/user/README.md b/app/server/services/user/README.md similarity index 100% rename from app/server/user/README.md rename to app/server/services/user/README.md diff --git a/app/server/user/bun.lock b/app/server/services/user/bun.lock similarity index 100% rename from app/server/user/bun.lock rename to app/server/services/user/bun.lock diff --git a/app/server/services/user/bun.lockb b/app/server/services/user/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..57ce3ec74dda46289f1a78a0d3625d5f681ad46a GIT binary patch literal 115888 zcmeFacRbeL|37}wWseZa3P~9m85!9b*_9Qtx9piMyQnB-rAW!j&MbsvBrPLZk&#&u zQR#b5uIKCh{Jd}P_bYn+e*eAC&C~rl<9>fU&f|>dxt`aRIM@ZeJv{_09b5!#TzuFp zy#@o}zgU=I#u{q=BVdLqEu?7`8 zFqlvsfYbnw0Hgva29O+}r;Up>6!G-3@B&;_~8@bI+p_Hbgw`LP3sleqR?er`5=4xX5Pq7Azq7QTFzuGW63AP@bz3ql0@ zxo-5|-on$>*9C)d0s7F-51lnUhX@1(_J0Q`2S1@dK{>=397^oMVCX=8C&3lFoJ1?Y@n;ngE1$tZi)E;JN~J8}$w@)_mS>nA`g|?5E(+ z4Q^!?UZAm)gR_GdCWULG-p140%i&kt+`VnURJ5|Ua5@dNV!1cQ8_a(`Fm`sKAP@Va z!?WS%Er8G;RWJ!a7edQHu>U^bht2nRlKx&OUJ6h=DpbP;LMa&TD0XjXXC%Fcm@%0{hSoG7lUbyc|H&zvhD* zcnzdOeS|jl1!m!mevls8SYNgPVf?}V&Szs`WgjiFQC|ewL4QsI2OwV!ltVjDK_1SV z6i~kt;DGqXeEKH3p_3)Cv5rI}H~7N1axnfLHg+~=nt=|~-3#jB`q%*wu5(gc`((f; z1Njet2f7v-DYaqeBS4r>k>22a0|?{z35P)6)5Z(V7d~eT&}j!x3l9$qKg?TT54MW{ z?cn;(mEF*B26=jrHvV{C1_N$CVFgZ^U2j*v33eJh)N+0pJeMPWbo+&)U_>%frFN z<^?E+{?-75@x6u19{}qBjvJBAhJCPo{<2d6@@$}7U3WuI4j^nN1`v)%7O01I4D>ea zB!E2ha|z_3pR_;+_S0K`qkVxq9;t750Y0Bvi;_FP>+ZoL@&kj%*K)P`eb!vu>Uh!~ z@K(zBl}M%q9v~QRT6uSXKh{ zoXMR1?egaC`@NTr3gBHyGVQQfYI=P8(~p)rE}AKIVq>Ing@bV|31s9%e7-UZHVsbk z{M4z_^t*;%+lgE(R(K%pMK95o{D!g7BG7F3cHudd@&)yc;~SL?cKBYp6^idH2cmH|20P5((vxt z;)zS*XGBf~hgNciwH3+KSnEo$ty1i{PbZi0@uJ~O${8-cw47GaX3d%jXZkbYw$AB` zofJF28hwl#3Hm@`#o_VB{QJJI9xv6GDe7dzt$da6v z($47aTt&vI$)3?HJFsKd&482(Il4mEI9qMJX>^+SrJ8J}&g;}DM&4S#Zi|a)EmlZLK4U@JeErR{{eeE;8a*?rdSx|U-Q)~C zz#C~}S*;ryP(+pURiE+b3)W9%ZITDobs5hN7kEC<%P+*={%Nw%FTr5cedS%uTdxwW zc4A+>`n;<|T?Gt;278&fd=gtfh!l7i56xc8EY3fg@pNIv_?TWgU&_s)>BF_49E3QeDAY3sAecI2n2(b|R(RfSP=S>}w{q=dCJlV)m`e7{`z>v?h=YMi%GqrwDC1 z#d|)ATgLL77$t!T9aTxK|0S1X8B_cRuNR1ijiTB8CGIajen~cA8E}Q~k;|&YS53zg zOlc{5>ol+2T{e`(u@4hA#r!i7ie9{eT$aJs?oy@Y{nlxBV^7zm(7b9MH8W<#Pk5_}SNg&{8h;=vF!`LY%QL#8^0${9 zr-Z#~X`o_M>sV3t@Lo5q#O(PH8C&=K8rhqrt0ybS++|Ha`>Y+g zS@5wkCO(CDT>KVga<%t`TDflKh5?=O=G(3*5mUE!H@vGoGkv^I`G@RBa(YMmmus4I z7tgS-zv&#xNFhDMS{=6#kfp6=QTWupQaS5->j6asiLw(0qvpSZ@nOg4IW zTK!4_#k55sfwWh4H%4N~=ax!r00E6xRYsa}D(3mI<{v6!W37tyU6q$)v|eRr>K+qL zYFjtCWH9qmxi_QTj%uz3Yxcl=sy-Tjh$PFfQfac^v}(stQ2i?_37R+bN&GaSw(nPn z+I-CK7o4=8vAz_hG_J;<%+5V6zlWb$`K-@WA3kfq%4fbuX4$q^@$PNo95tLW)f&e!K+X)e7Dd z)i;mr^H+7@TDUXvbx32_<$anU#~{xcKHix?9>yt-5M}PgSvJ~YvVP|?krl7hE6k$b zI-1oT((vcyzq!D_E3!4E?qu%RfPr|yMZW4mDvhW5QXX9AoKqjLCs@=q3jM5L(IPls zvmJY5)J%vubk6$yhvk?64NAFd(@X(6C;P)6?b`Y5Q_%B6vv^t^bZYH6(cAk7R0cnW zb{yl`x0_`8H`hx_?kk4F9Wy}Brh%uB9=ce=^=FErFXk> zO2==$ETIISBK{d>CC5@D;tgx4?puE@US@GyM|+j8z9WZg422{?b*oogM zL#N+4c7UVVRGuH--pOu-`Me&tGT-y2`MQFoiDBneojG~qlcL(R5=k7g2l`#484?7H zF6_H6EkWWc)iQlNwYI(~$GEk$e~4G*Vj{Epp--p#7yDv=*j-vymCRA-JRHYR6vg^N zC&r|T*DH*?h|MgwjHc>Q*B)E#EGgOn=9jd&M#s-lkLSC$6LLRP7d*QZFl*NtLo>4n z51ecMNrF#_U`hnnA#gq-00_%AtBde`0RbEpLlGW=KbLnkK{K+291#R$$$^uO8zmw9cLfL9Xz7la{Si-AC4a~Zpb|PT|W^2#R)MO@Mz?Z z{r3QT*nb#zcx~1GZNNv?ALMS8KLGf0IR9Y`kT`&sf18m0GlS>ja)6KUKsYy*5dJB^ zN7fH4L&|?Ag0#C0`0)OP_7&w1n|Nq3kA>i`?KA48^ zdCivR-(|p;!}-6JJ|O<@1`mh8qpQFAkCX#f{x%`)A_4y>ioaPO5Pm1%E8+OyIU8oH z`!5Z67^Z;ZBXjU?9miE7_8$N~cr^CM{igvB3laN&GXA=N5BE>V{S*H$0=@>y|E-j0Z2=#-|7@0vwtpM&k@&&#&1xX_rvYE?AHFX{RhJTon53Ju}=#g;zRpL9`50r zNl1H3z(>XpY-L*>zw0MBFU9$f z%zvc(zu85~k#-Y+ua1lVpS*uc1|QbI^#jk1aQ<)h9)Q??2l(KlxKPO54=$t}y!_jQ zw5z1WU_5Z~hXf=Je@h0H2%j5#I0rsz`fLB#DnA(TPXIm~yFXd~Gk~uJ_%LozW2^C# z*oncY;_QQ4;a~j%FaI_n{SO0tc>jfsp?#zr=Kn4s?VbTX_((YvwgppZ(+S}-fDaMj z{TJDH!FIN(gz(J({}|vy-?x$wei7iydI zZ_FQP`%mm^0Y0*R5g&eM8`dNKM*%+a`~%wGY(n@?0skC)%?y{~(EHHfbcZ|UkdmS z^U!}}9zw0ZO9=lQ;KTkS^A739-*I6b!tVimxPFlQ?|4W((w>`h{4aTg?*sTpQT}gr z{OSN7t{)gXs7df&?EQN&V*e}P!}BYwhZ9d;Vbs_|X1l{f2(Rg!o?s_;CJj z*6$7WzxZ=F{$}I;?|fdajrseh{#yY)^8R77YaiYJ+klVW{}BIvhe-TJarTk?X4f&& zei!$K|Il}$-x)>KBYZ8uhwB%PA2Nqg&Hn34gr5TVihvJoBW3@UgsMQ=4gx;hfBtm- z(DD3h{%tmP&<(`CIp8Y*`$&GXa{y^y0Qm6!0eSG+Y(EhG6yTczJ~7~edAR9>@OyYM z7^8o{7d(K$ocss;7{FKl2mEJ%ulo=9d-yOIIlzbU*lg@!kdgRV0=_ihBW0U?4}$P> z03SI&L5Ed2r+T1AKPB*|`t>M8;nU@U?+`ICnU~wN?M`gPRu|zs>p$ zZNr4v-wz%>fPZrKi@yt$Z&nB4Gl5MTe*Oh{@S+9RW)i~p27EaFh+U-ocN$2$TEK_* zPuL#Df2;9Z1bldYf%c&$9EZ&$#Qs6>@CiBpLLOWPn@I>i81S`F_L1`6X&~)J03Y7J z{xp8#Ab7`ce5kS2{VN9W;r$D-zt#9H13rBI1o?=@@A?kwk^Y|mAD$ue=TG=mfDb=^ zMC@-?1FYK7Tirk8|Ba96Y}S4d;KTbr!rSaU z7-?Vf5BcMO4{jlU+288@hY8$#Vf_Ad{TKi~djCTDwb}U90zRBS2yd(Nhe+;U=MSV0 zzq1YN5ql_gqnCd7Yf`TyJht=|9d0zPv8g_>KP zzbk-`-alasHj@zl9*VqXVL9tE8JKjB{meB}QByFR1q5&K<$kDR~I^}lIzu{BOqDNBlu>v;I@6{OkM(Ih!2=#EvrHD}eEXenak7&p$DMkMtkf-)jE{ z03V)T5P!BB|Gj`L2lz<85gq8u-zB7-HQ>YfgV=>*xS53T^8g>Y|3ltZ`#%BrNc@mK zY{kB$>c8ec(ub|^LjfN-Km3XPr+|-~U!dL1Y9R634Hh4okCgwe4@f&Vzy~4xxO&7+8{ot9Ga)F4itRo+W;Tge<6Rf^BD1;9o&47^Y5RGpD*Ae_Mzrh``-olu>as| z+`nSK*%%=H?*NB?dBBJJ9u>GYn-IP(;KTh7@f+SpHj@y358zwi_|W&Q`fmyjKi~-X z*Z#59{re8!!|#tmyU?et+8+RX5x&iOE@L~oRH@IN>PYCCaBRTiwqNO= zAk>xHkokph%)l7_N)X{54sI*I?nhvI_>~~Set>ZOn&Sq)N->!Kgis&sNx%Fx!J#EU z$hQR-Ob|)IF*Hgdj4v)fsp47E;w#yaODtTJ3ny2`T!gT;V=XsOlXAd z&i*RhKp5vJT>kG6+KI-sgGdOj3%EQ)4saFY@@RziN^#|AgnDJ*f>@5L|4#_}TZyZO z2-{bI3+mQ@3#R{su>JwKAl88k^6J3_6GWJA02i!p1Q$#YVR;kGfCLfFkEbvL5=26SOVHyD!^m7bcu>L)`U_v7-AOBUlfp9*|;PQWm(4RS6JBUzc z9)}A!{0I;xG{SbDzy;DY%jaKQu-=D*-@86Zp$;ktvBFuquTupI&Xg+r)Ege!-C zZye@HarF@4OEMgixnbGrZ%0fc`q9P-(5c`kr3F8lyt`cJq6l*{9I@b97@ z0(n1xP%ji9_`#gTVHnJS1QGTp3Lw;v!_{8^2*)iRmrno)6GYhlDnPj2Qvt$u={U^5 z)!zmP{m;f>4i57Gf*(vi{Dnhkw*=(jJ*XNW1;7@7&`vu*sPhycOn-;4NhkOP{d$RO z4-vk61%5%DZh)}99~R;e+8@E?A^w`LxcnGE`0_pY1>-n_%g+LYI-dZ-_DeWi0SNv6 z1`xK#2S%Y?EcgZ65#bR2xqMho4t_x$YJkuWdVrA61Q530jVs>^5J+M;0m1|k)^p*I z7a*+S!{r5WC=3wliQ)2+IF!br96;!&3PA9KQ3bzX{Pkf5htQ58E)Nmvd9t4qeWSFe@A}vd(b$3T(8ta5F62LCE(n>Sbm2V|N!X0Y)}v4F=JI5} zy*ScrFix-rm#dIx2lS>OB13A@K-m25J&TT7xO-CbHAr`+l1)b!+g zpx_11@QA-K#z?}d`HA&L`kiK1k4hg-P*kk0yH+7+CjI=9)rrZ)pZjV8Vr%xX7h}u` z^a`>B3BNv$b3QKM-plPGaD0LMP`}x+A(SpWdm;(Dy{=H_;rvz3neBS?^kETigb(VS zVlQ>e2qeD}{zgjnR+O!T0Z&o~9bFtkDY!90D^73@XN$M?+iHJzOOTDDZf9>s*I4>W;Hi`DdOg!DhhA|F> z*9*m8azxmNrJNWY@GZ1ay6`N6By11Kn~>ti9%0OmB4gUbAFIR{Yh|`ew;WP?dFk4t z&?9Eatj@$rMvol4uBG%%JreS-Z9er%XUhCtw8`R^lxPM~lrDU)fg~*DC!xu!zT~^- zFMHX4i@C@kV~$1*MCAeu>>(b>PV~Z(dx+4SOZ0?C2eL53Sq^W)Y+^jZ>u0p{PmJO0_tr99I_T zJm5w)l*4wqv3|LOR)WAFgh9g3aa*w(N*6w}K@v98(DO1yvk0-m6{o@aQj$l*&yUFn zzhOS!n8M6(iMyEd6iJk3e%!8fz0d=mGA^|)`@}98?IZZkZCfSzopg8bWt8r(X9+lA z?8&G5ma2}QxODCKZOyeM7i!6<#g}Zh$$CfMu>U2Y`*6lt2<^!C7q-Get^=TwN4^RmmCoVh~=vi_R(Y?J)Co- zw~^bs%9W3HU(WjVv3!nKBt6YpqaITB7j9%3Wk1B*6A*c%hr~(U#p|Z2ddcZs<-8f^ zcu&Ch(#ZUU?=_Kxb%NmCiPijyY3Zk|ub~VkQ_?71^k)Lt5&ail1nbWGZL>mYcfKz_*>|a{ z$$aN<{wHyBQHMK)A{>>vhizDcJ0(s9xJs<-CRO$SxTocYR>l-J$vxS2^%RuucEnkL z*sL@fsm#6MA6w<#M2~rUtxx9j*LdnpTAyI$+x52J&)+P2mV^7lxLRCD-@+p;ecf`E zy1g#5<1va|_oQTw5hZWUqf?Rmt1m z`gAWbHbnARWh+&AqGHjB2rPaFaS=)vKEpy1R)w{vt;PSjXKGGqXzX1g#al58QuMRS`*Dt{KQ|@x_&`I;K5qh#bGiE)hWe&PK5-?>7nY?Y`SLc z)K?1w=?6)5m}OA9bZFf}dkgQ}p!9!NTvWulE4nmo^#J=7lFV*G3AU8)x_7nddHt0a z@~T*L7*xgEEic?k(HA7S=h#xVey*b_u9oWudL8dX>mE`4!Lq9=?Uu`~NbfU-wh!V1 ztd-nr2U`oC-V%Bg_R{F--p6Ii6O-#>TYQnQu5#OPDV4O&(`S=l;B4rWm>uag{+Kg;YRt>SN^uS;xV*K7G^Onk_%M z7WKB8Xh%ox1x8U&rj~3aH~*D>$LGQ;l@9&Q_u65L4QCc>!&V;<-JSi zH+7w&W}b7Jd~|n@Or5AY^wLcnrOSlY%~;oq$F?U9Fo%7&e7Puc_v6Y<^Ga&^WWA7Z z(O3zsLrOQ-s4DH(M<1)+=iu#aH&Lz^ydZ{`bZg4sooG>4Y6&Vb z$iKPguQQ=^SrAcxSV!Ld1;Sr<^_G87A>HZi=e$TEMJG6Vn2h00vEk~Ip&y4B-CRt9 z;z$fvm1=sjj;`E!`ue;+)3w6kBV}&=Lk95w+C=8VZnQ4%_2+RCHjf=x=We)jc6)ti z*{RWAGW}h7L8EO)xK})rIDhN}b>z8Dyb`jWmtUwzYkfw9FL2%)^2{jp?rWC7qI6l& zy60Vw#K}FMOp^|*!&(W4)@#J&S>dUs`ZFoH((ipLb*JPUo5~4UP2;sZew){i{AYxV z1#-D{NWM)gJ!BSsnhc*oBmT0XbuCiGcsnm|FSl&x!@gGBeoJU^Ud=SyE1u(}kaWqD zaJ^oWnYZIFkIwgdh?#F|wxe=#uYLG5VYeo;E9rHGs>BMEE<0K`TdKl2=0xe_N23$P z(p`53n*zR(ol7h%>{4dgd*qJXq48-+SxcSKQQPm11bcM_@aXRc9-19$U364d#|$yg zaietipmo3Ze$zhi>dEcOHX1Fb5xRvT|Dq?nZ^yDeVyb?TD3Q5=zh01R`Qpe(b^Yg~o>I&~|XKOu#Rpi6$6)C%;WWO8p1ZUcScP(w)muJ%7h0%}n zOk_@FIIl$+vE*G%d$ox1$@YP4cDXo{v?BAp==;S!w5~2!qv%)ik_VOd#q3`>yuJ8J zHh{E$TKGw7;yZu89hR)zOmDB-{m}lzBVKz}nc#ygp`@06L!(ew!#eL-uCoT{`}}^i zu5sZgzbQ$dQZvJ$;WKVGgR7c?d0h`|Gk>9KFpQ^reD9|f%OpjE3_)+Yvpf;s$ll>q zRT<^9g!c-!H+T1`83&-^%Z1kcvXrHoci_j;QQh;wkN4Q@$Te%ErkO2s=FT|Fo*P{% zxtxAaJ_e&Ib77yP_~J*)Bgc2JkOZa6jn-95H*z`Qb^hafb@2iF zBzKw*os(Y{EGS!-1LgoQZU zuU9>8Fz@+%^(L#dF(09`!0nJEL6YN2ozEkqZC(=V=<@MjFGJ}bK%XGTAL&ELrd!+R# z3ie$?zc1uN>wb@%bH&^H-m9xsLx?*#lTn$zxy#N)riy6lj&4fhkKJcqCaf|=`nsy= zIn^~0%J5yLlFPpK{Di?8{hjfplKCE#zx-%jT6>~?R$;Eb?B$bVb3J$Cbq3h>kS_7~ zxwu~M$I@7gy}OfHy*k5tU}QAnJnzl1p{e8NI_(Vdepmn<&I(_kd zIP=(39?ph`XLD!aHI7t$ZrLB3@Vd!-p;&+{&bouQFWB#)!3X9-ao?&)Y1#vvHJ^F* zMTv58tKC7L*9FnKB_cNoCFu!oO7Htj%04;WbpH_X{IJWU*R3;$jwds^5b z?047oq_+eEk0YgerPPPY!WwFjw2mVNu5 zO14|j=7o4({6OEo66mCi(iKMQzNFFp!gi_7N|=q_aA%gbiEWKfPJrQyCATo?j*(LJ z_Ld=!uOI0-?-i*e=jiFlf0njV8Nc{xI|gsAazssn7QNm?(7NAvrC07tPp69WvNjOL zE96m0)6A+m5QHmGF0UkfjbOB>_h1NDp2FrATc(|{@b^D>LoOw8zs@(0gA&K@+`e%U zF_)h`dJ`!-D@KIB8~aDg zh1cy!CsLBQjuyo7d^2phe)VNgy_0O`=yN%|noO=3nM51dzZt0=gAqm+-Lvh+Y$>{e1$oHC{Ja)WeWQteq(N~rfqvUSO zJ*V!99npWUoEaNCckO3(&bREtVGA$FurB!25zltZkK{Ua@?PO5JXw?Jn02i+Raor? zDh}{_QAommoSgkmKo#A@NZmf3e_hK=E`Ehahd#+>p)D)1$xQs7REVUxh8MNmM|ZbX z#qv0jHb%Fy3HzV^>*LnO9krE=DBZ(|C_wD>@|k0~6*^q8^VJrPY4x_I67<{56B1HM zuNkxXWKqNjCdOZUT7G-AaXWqhHrLGWy?yI}ly>LW_&wg*T~inflrH$U8h<71yqKGA zgmdvva?2zG^=+45L?3y6xGra!;5**-J;PGc4UzZa%br}YZ&AM=Co2^h@8;A^Ougn< zH)Tbag~@5|xsKA6{!1138?|Q7k;_#`qme$fyqI(%Ja@i#vZj{l@KfhU<|NgwJmIDI zn#Saw!Ub#Rl0(m8&uFAC?K9UoOq!xY(UZi!f``(TLF=|3tv2;245{-+6vHlU*%THNNn>56HPs9<7@ck^5c5$^ZP1 zhPsG{CUsix_s{rh#D9IW-X3InlwCu)dns32{giG=`uxIYs>5*y?>~}%SH&&2bV|hw z|KV(xJ4#mptxKFwvPS8vkVYYsy?EkDyFQUGe_&N>w)gFI9W}kyu{=srQ}b_ z+S&_Gl}PM+MEi==*UQS}8JEb9)ysM)-6Lq-izcpi!cFHrX1>G~o6Xpl z+?Sl481T$p(Ip8e`W_7?pJcAQ=7JNHpNFP`*+OG}5Q}5!@&`3<4Zn6s+t1Y8UWGodtD$v6 z89#}2v57~os8sd8FLFxSX4GcD5L$Ry;z&Z;>9d_;G1Mzp?PIiV^Xg==bbZ)8)i3n8 zp+tw@tu{gX1w#uq9ObV%TDP@+mcPcf{7@tfCr3{#wQNLdU&G>SB9p!KS$3tD^FF#< z<2cijy=yXz)oYk%d-K%k9MSVe+|dS~LQ8TVy=Sp|5omVufh%k@9ljeal`B#OO$AOfi$k;dRU(|KDeWK{?-f;ObF4eRm z4Qy${>q*`C(1Lx6eR_G>pBc;ZYjvB|jdZ=K3l{XLK1ZT-wb8n#Da=I+uHAVldto14 zxUk>ths+_OB7N2?B8;ZmAA4O{LV9<<;icC0?fFCJkncePqge~Cng#g)J%W1=y?xwt~d!K@Ji*B|$uf%hC z4*9jOa!P#fjh4)NMdkIN#Iq9pJPQ0Xi@y?fe~teTttGycsK`OVCcWC&;B}%syb@N` zCRj{odbdR5lGH$2W734dqSxd>xh|~q@dFX=!S@D_)bU=+BiI(hii*RDzf{3}^NXzR z1?QO;O4XjahZ&?euca9@GW8{EJdPG-wR`c6j^N(fgsIFS?YJF3jrA4V(w!S@a?)QJQmM(nuA>JgI&EZ2kRq z`gK#1JXZ_u=o5znI8kxXN9&G-Oji`YzNPl0*(R~v7i_iuNlJbA;^pr~f{Fi+Cweg~Q9y;OYt4~8>~R^_PAMpKPGG0>=Vph7>7 zI*HabHAu|ta%r%czTiafp!f#g>8YE(-{MWGZKoHW=B6fV^gd~b)~!?7CR1*hrOUuqy{cGpDSLZQa)jhW-cBQWLhhe$ z$ER&BE9aZ3KJ03DAjv}@*TYCu2%jHTWVBrN=;nI8sB=C@`viAz(a)-)E8O~+`rshzi%$bm zMGE8cV$jd!OwqdU?)woe`J~=`u8~q_kftr&yKbA7858@e3gZ{v#(d8+<&MbDBjj{S z<{poN?4Q-$p&1)=3Usqiv0ZY><~Yr&it^VCt;LABj4&FNoJ%?+GLv6H703C%fK=PicD1O;;18YmV0ausE=~ zdrvg!rK#cmJwI;tY$N)tbM?M8(XEWC*20{lqI#Jf+4xTy^CakRAs&jr1{ebx@6nQZm*hN_*~M2BA~iHuD#O4kytySSjAJMorY(qQR*uwmiFw><@W zVsi@i<9Iq+`gyr@n&AB` za^zIyZpp{^&j)YM@2EZCfYP-?>qa%`h^obFNbNQ1Wih{S>s7XFx}0M1i*0*zdVILZ z9!?2;mYd&;eNfHda9M@wE<3$|a!9t|=l5>5LLv(L?Rn7t+M{(dbCm1G2o-cbuJg8< z@9I{&9~xx&$X~s>;!5+I#Y_AG*H~6s4Qn&|pJzqyW!Kp8G$cVf?jX^KPPS&uu1BTC z_fh^jpmi^0H5@seSX8fVOJyY+UmeU47!abA!tm^3!)l^KPl29PtVQiYzC3UJUD?J$ zdBs<)gu0KGcuo=X>IQ2#g(#rk&pV=Z3yYk-Ua<&Kz>hk7#Eijaf0cMol=V2bK}9+l zD|zVnldQ(z1s==X(JM2t+1NARmZi0-i^+DMy+5~ydxHE}?^%?;r_s8i*Uvc?n+a6+ zG@P%a6m@3yQ61#CJrq#S9TOtaGvQ_A6K1Ey-oD~#j+^f{gST( ztf^^pS}0v7wC*L}Zho>?S?RTofyu2`qBO&dnPw)_2?chjR;M~JcMH|sq~gw&ixmu8 za=5|bqNi}^?Bytix+@38Z8UBZhO?pPuQOWr8huEES?rZzBHM_@3cb_z0cO!x`_2n- zIO+{tsbxIzHZ`5?^MR*!yJ?fCgdAC^zNIz?ZdVUW&9!b6yt>Hr5q&;$LF+!QXk<-N zu+Y-^Sc#2d%AEBG{OCPz>|em)Bl2`YiF;X*rCmS9TXsM5`x#gH!j`mkt351amMhz~ ze|sjOxKgQ)ii0a!H^e`FIYMYzIy8d5p0!(HFM05kkTVCc(qS>O9iY zEYDQT2Y$F&%6wyRAm77Ak#kSKQw&ce@EuCm4Xrz0n%Kd&a^%$YmG)z=1^4j}(D!O9 z%?Ys@>D{;JmohT3jMcrdd;g7S`6q!Zv2PaXc?*`xo?PeYGArPX-M2bOh|+aO>pqMP zI3}(8#a^;E=y3 zWNJ-v>nq(~d$1Fw>w(slusZT-s#5=z!t(C!8MT`sURN_qW>$v6w22Q^R|(0VsNtM-w=4eM#iqWtZUFUVY9Un4>?ekpnU21cEPm=3d>iRyle^7e%%H1SY zLSyQqVGSOi@LV+R(^I2#z0tbwrQ*ltRH-$FE{xvaYl-i%-?Z%u6XOAM^}f!tvwk;n zcdoV^HcWOWjg{8HCMDdtAAc#s zhL{MIKP-CTf&xx#05#=^=_mpJ&PE2{2AI@S& z>H4B|i@To%X53vkKlh0SpR1!y{eHW_m-owZhqv!J{n^s;idQWevCvSi^<}5a{qOOH z#LtD_(ezFVxh<$#tGd8sWrN-y&Y*P#pU5eXD;^QQpgDTvqd0k(0v)h3$ zw&)f5xrRSlcc0Buy8`|P60tGv`;rE3Kg+5~!ZXhNLC6%g*UdIU^mzigM4Z0g=lN$U z#W6%A6?v~H;#$8R$vdiZ0$ahSc?tbKHUO<_#!US2NTwd~&bv9`0&{O))=P#&`{M#CNfVct;8Xm-cOG~2FA^Np(t1q(5eF?C+&Do_)m;k{QpILR4p zd^or)nEKoYyluB9j_#D3yHTjB!cdfSW;PS08;sViHi?(8T26PG?XZoz+mdju%JRz_ zPx^$B=)`DyR;_afBU(j9_g*h~Ia}RLFxX~0hw*ed9og2YaQ(Dr8Mlh>1C(wES~ouM z^xH!Ymc6zGgN~GvGiJ-)_gJi%8a}O&aPuCo^7AV)eY($0(?Zi+-$G;jUbbNG_msQY z{1g|zS-yQMreaHu(hWuHE-a+)WIFXEXmx1BD8HdeCETT%{vMHFt|1 z8Ol#`6I3Nm^ z>tajAv`lo2Jr|N5at`xUS{3eDE#Q9SU$ATE&dX|X6+XRTW!t`SeJi9Zzx;076CooD z_bh4``?Gm!B9BN#T1TBwy5VSD#Xt@In>N=oTUZ!IzTjCXaxj?_(PaleEVYjExHmjp zV0BhcUu)ObyjyQIsvYNeIX<%9WT#N9VkhKqP%GQJn**gAf!3Wme{9||@i9GvNGYW= z3wu|!dr97jqfr#r*XgC~B41i}=U7IvpZP*TM4l;Kq3)&S_8@uv!IHG?HM^ctBir<4 zlx`$imz;BcD*9b++kv@5YF6%_?grN6C7&GMW0-nBj=z4ptB)r3yAmAM3Cxu=5K~3Nv3*kP;OFA3ZzA zGHPG%YdNUpp;$e*ciRqs{RE?(x4pYv-A-FPqIf68I2rHfhSH5j>t=*6UT(V*e(7l; zSE{*H-7Tdi?FVx=zl+hNx&EA@VSmNK(`f&49zQ5tY47*CcYEThc%qt|4qo4$sMso2 z*;4O}(gj~U{3~Je4YT${-q#Bze#pc8pliWdyy#3u)s^1ZdpVsn?+a3;vX35Qx@~7t zk@PHMsBahPSzb@&wdtJC`>Lcw=04SinWJ>i{iOIBx@a`ezfc|p|x%Mwpk_;I{iUbn|nNkuY}iJ?r{nd z1tm)D^gBE9sEW_2_9x2UShQ};vf76U4p&;2p^-B`Y<+1S5ep`0F8X1vZT=CaktO&a_kZ-3OsB_awg@CPF2c;W_*5%8xx=)u#aH;O;Zo3_e zZti*H?Yft)ysxwNl<$@rbZXSN$)DctFUL64yZ$Cb{(PlIbi{O^O7^v>zLL_^l4|tx zvkPcl&G6LgogbODDVJRFao6pWBK!U?5w=&A{ccmVsyfIVF_87ty-+3=h<`b&|XcxkY__X4m#6jzE9TCNpvK$jLDbR_1u#;(x9W9j+S|`7S)!pL&t7(S(wR z?UZ%U*O`y#@9|$j>+awt(Owee5$W;hcL)t-3*sm*zvE(a^j&ECmwB}-HCCKcL<<87 zG*`zD=Ux@gbbG9U_llgKLh@|A9i14>ss{S|C5dR=&f=hgJ#yMcZ!dBlakY2kI^Jpi zz`R16;yAP6K$P2dcmG2J#wLmQH@t4Wu0HrRbSm=I_|Ei-w*+zrVoa?QY0%%7yNcFT zqQ;kL{dRuglWQueMF#;3j|1(S*-Ps_61p$A>x5-f_nQ$Y4U5{QNK&{>wvde^P$sqi z&~I+Ho|my zs}AxD2F_-0D;QEVx4U;k-Tn+&SNS}ph@YIb#Mt<8yJO(1XhE3CId70y1rbvp=vPqiM_l` z+nWL% zWz5y#mMVJvW|;5Qg=+pJg0nVcVjc_3{HGJT%t=le7q+(~&shb2PZLX%j1Keo`r$nW zrJIJ<)itzW;68IJzT73XEhbj`h_2ZQ>Tov3y>8zbX@iAgs8U5%?)d)H^Ya;%nmZ-H z!>hoj-1B+eufe*Pm8-iz3;n&p>uBA;J$a4R@K5qUdQQZ-AlHPBLnf~E(Qnp zes!-E=_8Sfxytf*hsddm$2#cdt!LZC!)=YGKMb`c)dp5Kh>;Ygx!vI!IThG?gx2ep z%{cs<5y*4!8))4L>_n)tZ0W6l%@nL=HimV(oj!JKi%37PK3L*Fe9HR{Q(=zG zxT^_!q)h5bJ6Q&b)N=1AUMJs2BzjEnZ#E#hH_^J~QGNHX&AZKr5K!kyr5uR3y34En zGI{WzZWr4nfe7XF9=xL+R3{X_Y2J1WjIJc^kqqSJ4z+9`iF>xg_a0zTEKS=mAczqVQN(~K z1{9YZjHsxXFrt{l0!z-jBqJgMX3V0PFd-;n77VCh!mMBf6fx(3@hJXx&Ca@`GKu^l#r0)bZ>F?bt#Vmybc62P z=XW-6<+ds`ZE9$@K%|@!cl^bG0nK989O!@MN-fuug-f*e9VttB6uydcqJ=#U)9ZJamq%?8zX}4OpZXV>m=d+P3H(E*a zQQNLB&#CphvminL+pXD4_G`~*eXU2KsQ>dmV>@6v2F5>9Bi7U726{{s%4Nq9y+OTq$ZT|?Rjirm)J#6^uWN_XHmy%(8L z>t@i1D|WLVbsXO3S+x!N`6vBsyFWVhHMzE#L{%xE<;meGmmd|WJH(y2lvZs-VT1i= zF86J=*`6bJ3s-JT^LvN1o%P$lG2QN{uJrv{So13ClQmopTv^!d)TN@mPn+%)+_Jjc zuO!N(XV)C{K@C^EPRQPT;C0g>5hosWod48}BX=uTZfToBpW}-kr)zolzjDS;bHJ0c zHiK;Asx<~ihB=fT+ zwZ2u|u+)6E$N9_ScP<*s^_{j>0*jQi~N3< z-z2}w3wGYcU1z0o<=Wp!+uAe5X}nu#tbUGDw-)D4wCH{NPQ4n|<5g;o^&OUbruhbo z!WLcAw(qL$DxP*AtZYQ^6LCvTkG(Cdy)`TY0sD;AC(SJa@lj%JS)o4WrPw&=%AzkBoh4=G%dr1o+# zcfP)ZEBAcJag(}b-JcBYdq}(VpU?u&F3;y5ylh$cRB7E?Z@1)LZ+jdl4oSSZJ62=o z>O$_;a&BkT>XY-I>jxn^XT>D z^L>UiI=?vW`jgI~BTgR}ZK_i*ZT!K)ocDfPKJM>nc#0!;7guhc)?T$C!v0?+DFq!T zO`0B4=q?e1| znf7($$lcAAyMEhHozt~WEHk+E*kiFy`^WQ|j&9jCbKHse6`1vJ+8MolFgC3hbwoB>e&0*;bqybBU>ykt-iJYeDlIR zjrv5WTUd<`Ppj*Nwn2A(bO6JAEySGxNKT6k^4T`KCayDbxjRBcvU&~weEx2?sM!u2R2AK@qDqy`M&R( zCuX0F@iN+}apTLi6~mUc`=a{wlli9e=htZj*RmBQg-w|6d*s?@j=uZ3a!-6PteO{N zn{}w4rB~_?$6M`fANSJJoRoQU#wfwwZoQqaS`1FkU%qzCy}qBjbh+}XnL$i{wVdox zuWH4{y&i0{@d8Kg0j}I1uZGvNZPub=y@jF0Ny(W*UTg{MVpXeFqwB$e-oBR(2emuD zR&CSx#a7qzd>vtUy4l)@3G=Um;;w1ZJ{XwO2fpB?1NUE?w}uYFO+-W$K0b>1wo zUETIo+r`l?HFj>*6kVFRP`~SikxrtFIVAyIU9SzE{l0m8m}mQWqc!4Z+7IxHd$3ED zd*45UD|f)YPpi7Nz8Y(%zW`yb>()praKIq7f+ez^v~PvpK7`{Tj}%6H_%JP z>Ppjqx7+4!-M(((b*tU;jGk`rJ;R-+W^(0zRNnQ(?Pc{UoeCPXYNB>m!{z0)_SXhm z?cS(!!hfyv{@_w(IHOZW)NVJ2lO8ok>&{BP+oSdOC9A?s=WV_} z>($z+BNuMFaHym>=tu0$g)-m}ZLY3h5p?@1it%B^}uzh7=c`wl}p z>uxbuF?MxoaJ8i4QOUlUD;{Nh)Yw0zzn+8Ro$=o~Zz^t|Qk$Pj7 zGZMS%Ze05w<;q>Qa_sV_(Ru#MCU&jYQpX`y8*t^rF@a6+^zM8Ke^T|6mMyqTB#|~LsxyREcZ4Ag+x}-sh*~W&wp3gnF z6vBgSvoh@DTN)I$#J0|L}ikp_xu8GN+zFI@)-Q&nT z#+AD+#_8_is;|%Nxi)1`&m^BFwxj%Bo|%0tMwX%=du<`5HHoe!qY8@f#{d2y$`R-tk8ApB`Jdl-m zNp)uJhuc@HG|6hk(Knkb_gcU9hWGM!JTA_9dgJQT;JKmq`vfkm{d%`anP=JBHQ#%Q z<{aFpe%$)Wme5S8# zp;uhkb&ja9V_B=(UBVMn24;R&f0UmUHs|&3_Omz5bUf{E*Wc=K?W|!rO6?P_)jx1g zzb$tk{3KVdaa?|GhvT6&3*K+(Sk+;!TaT%CCFv8N$qz3S^x(|RlU<&W7E{aC~E zwa3T!`SmWRmG!ji`B-y|Enex}mT|8pN8eLixe1@l0+K9y7|t1ZseSb-#+}p-&QNdY zc&~nI$M-GTgpM-I-r?1?liBU&qFGJDT*ue{cwa+5!OqR&e0Zi}WQ<{k1-8GOz6 zXny2;VRp06!Auj2ar zDC%tKzp;OD@}ps2x_xOir^zt`r437)6pV;8=EyzAmHW)pUC(WEs->fLiv8BY+~Yd? zGI}J34p`87OWE$UTb-?T-|~rjaHY?wbsrx`zuVWe>AuHBr+RIDT-T*Z53kOTuX3Nm zJI|GyxoSs#o2v2mu7A8!)ym&}pXA2gZb_GF4!zyY`ni)`ymJV)OPT)BNl^jaC^@=vQ1S3f^&T&SlvBvu?Tv2M4w zomERi+vm@GYS>Yd5Pa6$xU)CkBr(r}t+iez!2@@LhF+02c@wO=eZ30b-zgKJBEmS+J z@9e^Lx5ntL+Bf>$ZjRhbT)ACqHLJ5teb}bLp7{f_U!TftVrSXGens@8vq(^&9oW)vT^ONj539X?oe=#LkZyaW17hjR$MQMkk`p=e zM+?_PR9RfZz_*-JsOxK*YKxI(bbz<9D_6>z}s&>U5RR zR=q;+_)feYo4WYUAk}$xx+y!)d{Q}dsQ=eagLG$EZ#}T=Wu3i~G&uU^bLHCHQ|;Sh zsYOPgv5!w1n|<|P@~Y^%;BKQ5>%2ZaipLM?J0KwH@F@M#>g(UeluXPJtY}ocafzW> zr%tsWCJ35)6u0BZz0Q@pHK_6K*18LfC-+TlF?O~fI)NJ0k;!|<2KE?d#kgd0&=BA5Fo~jP=7YyCQeLm_2SFT@&ZaPg*s?BViG2ME` z?#~@^dq4QHS!dGbYHho%IM%YRrRAf6Ve2oyw#^Ocd~JEVzHxW-jndXU_bl%Icuc^D z>GQbb{7tT0qo{u(s@!d&YXAP>!;B=e7j^QoYs|fI{qnjvv5mUVKVx+_45-o8E;Zwy zgRe%8e6i(h?}?d%b}pQ5D5;b7W>Jfy+c4*9d#*cm*B76=7hN~nwd%5*Blk8} zZerC2$3NPpOu2E_zD1SwTJF|k_NJ{l=-ql>POE&kn7m`(i=&+Oz3_NpzW= zw%)h7HAd?W%(&MrTBqQ{j&g2X8HUC|d1nle+e%Q>fjnW>c5l zY?q&|eYA;yqi-QsuEWJl-5x)#_g=FkA=tIq_k)dJ#Vvebt(V?Hb!y7}%wf;_Y+e)d zwXR-R@nye5A0M1isgkTZ^=mEBJkh||@KL9I=!|07K5Y?KZg{5dm^Y_$$M3&XzkQ!p zaZwWC!Q8KDCWZ$clzN$}K06kjYO$?jvkrHLH{S3m=<~L0_whli-mdgYidwR$XD4lY z?sFA)xpMETPj+55zuV>T!AVg%!^dtNTraEGv183HR@+M&r*0RYxn`jo5q@OAwi*jI z4XAPcwD0?YUC%^JT)yVjsp|e6*K2ae<9l4WJ(rF<+_s}Z^-H1WF8#>J^v`K)6Y)XE zV8p{EemYrGEQGy&%b^vq1>Mos*C)D&g(*)n=5zgJXwcuH3i{ zU9vw;&S{rt7UL~x)aKbDt*n~S5xYJgx$PQgz4e?;-19b*I$h~~WVE~9(*P~8?}i2G zdKqpf^%D-57GLX?#C^~GK3A@CPxpu44JY>4Eot!2@an@4cPt4XDTuc1sk`WdjI;96LPV%6;*6)^eY~^lnuGdacZTGtXztwA|jAAxTw>JyX5@NqyA2>E|=U62ENE z@4e`9^Y{>(9`DCS_phSvf2(Xj(=`^KS8>O;hg`YIPP>hll+1P?_eHf@>`T{K*T0)2 zI_dV?{^HBwjZ4*qYibp}OTm?BgYvUkn@_GwxqYI$*}~lY>Y=vF1MQ=GbNkyPuH3{O zhetlM>lSxB-qzo7X3u*GYTxbq8n^zal^D=s>z$%(3XdJZd#OQGgN3Y}9;W1ZkvzDvGr+u|EbFWbQ!{L-S9Jx=pav!8rJLEcXYwJy~ zSKiogC1B0g<4-Im?wou$^Zwp;4t*Ewvo7s;UNfj(o%)O3oq7MpASP;gK+cz`{fey{ zoE)SxC1f>6?o+PZo~h2dxef`Yo#syb8nwx!YDOAv)B7upnAoCp&6}yGIdY$I<<_!2X5A;Lk*~`2E$Lm;QkHdE z-}3&KrtQve{`zg{?xXW^ns4{Lu%S@-hKHK^5AXNW>m2fbp|1Ys^`+;bvpV(cDSpC{ z`_=R(!`W>r1 z3|^`vnQ^OE2KT=IE3Vvysn;)jJvIE>ic{)GJ2^(|)=$jRGMed-RjfPi=8muD4eLF; zKkdi!fhys3I-S<2mF?^|Y@G9`7?H|Hq2OKbQ)dQq^nJ~ht7fV>&8J7hN%w@;k`vtr zOZNLNbJ4FR9Q5(_=yiQu^4i+CADM5Y^y09)(%UKijmmUJt4ylVDd0<#hx*1^&-1D- z~+|o|Ghy2-KN)%y>snhT*lLOWuMN@%)Bu_{hJbK_l<$qVp3|_bgDFU*Ox^Gg|VYmwubiX`JGsEw}x)~%{Xs{WJLGP&>PMD_aZ z;*{5X-X>Y)on^7=)F%UD2glSNIv1aYpXaU%KXT+H*eJ#9kBmRwQW3><0T)HnEtfJ|W_<7{H2YHDD?1tyQ-2eQ7VXpSc6-I#^eLr#K zCKc9SBRMin-7PX!zssvdjc-|x)U;bwa{Fn{#)u~QZ+09?z1;o!-kicFUN0t>UUCjr zzSQpNnAdMCEJl?b&O0W!#*zD(D|g6OeKU=92J6fQ%;-7PcJrDIanBEZ{ao~H>>^Y9 z`&E7Jib@)(q&-;KVOadtl4Ca0npEj=y>f~@bvSj{(+`u(GuPLc z;@#+CGo^2PIC8&oUTB|jq{u_xXz1e4{Hwi_;R5Bim0#QH7_Q&PLmwV zck1C3eSO)NtRB;2N6aw%Fhg87@sn`s>yOFiUxGPu|KZBL6PuYEWYe@{&Vku&)1O}m zJFC2Te8$CjW2!_BPwz3KKy~=2>dn8IzxV2=@*s7A?HiY%W4EidxK*_zaq8+$S9?zJ zV8T0l!*Poj9G$9Nbabq>eie)U8h4+M8`f-r)tml7z1r5`zDHHYmD}s; zuxTSy3r||MHEe5NQ{~XWS$407NumZ`Ik3|Ab8p+(9lcjp-`~1my|L4W1Lhror8KE1VZt)3nbRAuLPj6SVq>_)}5%%h~!|x}q&$zL{ zbMsX_f76>a>vXq2x}uhDj8UeM^VrOw)(I_*T-Mr;Z}oOBM=mDrKmSyhY_Q22`>tQk zoQFI1wb81gcK1TVutk%U2R>SSdTL(H&00AFdi2e>e`8Nb)TI`-dx!OKsrsy7!>Mtl zcjmgkcZQY{kktr*N zMt>}RUT?$bp{BKJ>pY!1chwS4qcf#ftrp*y;APX4BUgnhSG3BkG_cQuSJ8H=vjq;{ z*17hJ65$61cQ*D_JDA|O;N8iTAu-QZ-rxS&FZ7(g=YH3It+L-fUVqWO&5%T+<|97_ zapYFv%H64)HGhZ4$EXS(GA=<`R<#s)qP8w z`sTaupEi45`rL0~vnlgeXs*^;!F^wZp6QqUsfOi!%Q&YR(^PrxSfjD6%N(O-Hmf_g z(Xyx|4LZ-bXYc>T=C#49PcpHEZa(%gfbt#??;R_e?m_;+dvR5qF>rKXgb}xPwAI>2}xB-A8iTq3fi<`)@N1Q8&S?3PF77huwtx_;r!k2OD?rr zupu|`$)Oo5vIo@HcRkKs7t%X4vOm?T`?RiPUsHc)HonUN{Q~v*rXDX|H7gzdz4)lP zIPsz5l<_92$HHuMG}~pixniy|LF8NHv+vWvl7_BZJikXB_I(UlvUQ<4S8mhZZuXI@ z3Xd$Sb3b17MXxPQXDc5t?tZ0{kw9nh_BJLyAGJ(0ONQxoefDf)L4ysKRpR`okG$yn zG4j}E>&}@4hTP|%t8wMtO<276mEPDLrc;b#wam;8tQ|Mg^Mbeet?RJ?y05kSW!Y$- z8d+R@#k7apH|DB_oxd{6X?35%DV`Tp=C}4#oA#djJVAA?+^rje-sspDW{8mFeu$D{FoTxWh z$2fMV!IkT}Z^z>ib6U1bdf%tl_V~4(h4VKi2wE7otpDgq^6Ey@*Pf{Msnz4oFPE1* z6feB3d9HqX>ciuy<1a1?cN(#7$=NmwIC9B1WPhsF#}qypaxZ_)rH=9ab6!P1?^w2@ zd32AfUw14z=C$CV;B&uS?mDV<(=0CNf1H0XX^!iY>FXo3R$Pgi=D7H{u}AD#j@(*Y zxz)Zt-g)kc@wpREiv?#JzqVM}rf}>shX#$8&(3+ZrTLQ|z7wi8dVD8wTE-(Co!ea< zm(D%Dsif7!Zy8Q4?N6O5y};d1r8&0jPt~LADv{g9H@iAsoY&)@h9$AHCdAJ;EKHkw zWVT*PzE*=Jx8~1|xZBp=%dvjSf+9o5Nl(^0L^QNpHMnPd&f30fzS(m0r9C>?pKAJ+ zdKu%dX5F~cdG7R*`J2|8lo`yc@o4s-yv=JhFX@-Hp16M1%+wol< z(`uW2>-%SCTuw2*KI{9baI;>wLqFCe4 zTVKqnue$Ed$P5jG%k8?UD32}9y49{oB`s&v5kd9p$88_^c+Rkw)i6!(2=)VpnI;*Xo5vD>xt?J`;f zn7^JhAmDVDPHyW{;!Vbf+3wRd=jf}$mAfNnIO>i zYMmT^DkSjU)8j94YjNWljUBQ-RpZn}9lrP1ykt_l#_SsXyOp#%mmi~Kym8{R6INMY zmdrHF9ewgcjbO>x(wVbHu3d0wpI(le!2O%U*pm~Ndh0pd&gAH;%avPj=z44K483Q^ z#?2de+vBdFYGi?V}91{@kzFshb8wFeoGH*6PQ>ggv9FiE1i!7N&HQ#P+M+n$^15NeKLEdR4;@^6y-d8t|ION0%KCrZ0+rju|F3&MW&JBHP-%fm3shR5(gKwh zsI)+(1u892X@N=$R9c|Y0+kl1v_Pc=DlJfHfl3QhTAA#5cgz$KA_UWHKyRB(gKGFf$N)O%esTw==LZ z5c+y~_y@SxV2UjJA^oEZI&Y5N_flsl@@sm>urd207@ZwQ*Hr-7@0im0Y?MZ4;mCf& zk;Y-f^rH^=tI;{c(!vg!@#si-D5tDkDu?7&1xVLwKy`p> zgm*FNT}Y*!$eaXh1U3O106GVe&Mc(!1?en6I=7GNOm(8O>qtL6fa*FQG8O>wz+zwt zuoR%*qFxTH1QG!Hjbr*f;zVE#uohSctOqs#WTQ>MW`KV0bSpr=Bf1Sp0mxoEfHc4a z<I1q!13(XG2s8q4)QnOSKp((yElLJJGoU$O2($oN0oB_@P=YT9=KOlw<5+DQ!1;T)EAOe^KtOeEsk-!XKCJ+tG0%CwzAP$%f z%mI7=Utlya1{e!C10#TufD4E&}I)W597B3VbtxS-?HuKF|Z8-?r`r^a1(; z{eVHhU|=ZV1dIX30^set9iaY7 z@yrHj1W+HQ{#_lQzN-gR1JnRjpb7w!F?^JlksEKAEn~@tt%cI>~Md5S{9-4^X{`PH~3HrTonR%18MPfEIu; z&>FA?S^*SytN=Ks%rfKy*r{Pji6ivUHL`G`gm; zC=Zp_91xW-SV!k# z(^x}ePE~;HLH&l#d#5&2#wP`>ANcqRd;~rKrNDN8bj<^H0Plf!z0?&Y5z*FD}a2PlPJO+*c*MW0@0B8;5 z1CM}*KnZXkC_pe3 z*YBSse>Xt-Q+~1)m7NVxdLBS+NOGxtNN1{N4nVT50wnh`a0$2woCin;vUx5*x}OBd zW@iCvo72E4fMk$N`XpQcNIuDw)rr#S`U*hbxpdNz((-_70M(W3PIlf4P+N1`DjnBU zPx2wEE7j8+AUVI&NG{cz@)4cNARBPYDB_E^5~lK&?nhQ zCX;Mo%1I-6l!xkU0XzU?K1B7UGKh!jNj@x-LwqEkWD(7Z1?831htlYp@{(-2rm`rF zYYr7z>+mA?>kOVDobw zZvN|D0h13y?z)zd?TlxMT(p$OTbpz+ff;L%1W@X$jcPKm$;`9WR46a~=>SB@t=i^D zf3q2-)l__i5|LOUoe^q)l9io2=FINfMb$`}nTd@wR@MY%k?H99t@d^40E&gRiMfdx zGXpaK<;?ku&l(OMZvskt6Kg9_MxY!$P~2~I-922;{-K;AJCQeISmflLTFIZ~x&?x# zE_m+0X;s+PHsFq&;^7q2nH)W6VsBw|RZj6{&;Fa8czuMbbFE`?%1lVB18FD3 z>kr=O>Ud2~5rqfhxG1rbpx>!pK_&tEDpXSo6NHK&F_`eD8$Q$BZ_WD)pxBt0Q+?b- zfo?>xT99skxV>*6C>F2|Tu3AldJyH1ho0*><&3tVn47dGX#xJOBBGp0w@+9ZF08>& zNEl8}^k<~mTzJxPYuc0V422A;6enaMAh?Vk=srP*R<=|4eG)Y9DNZ=(_5>NQnB>r<;ca{Qafx=+1 zC?wc7SS#zmyDwwoK`}S6Bu~Wlmn1|i5*If*Q5x-|{8rAB3yrIzefB2bpQLGw&^AGaTcDea4c3iI8i=i-W`VKrq zIotL2`J-{G_ksdTlWr8xh%zAIsBKA&d1Vi_(qkBQ&kgwYq z9>z!cZOv~T1#McM_cB`Z_$X}sZ#3ZVS^OT))23)S9X#h|BO4+*w*&)_Y?(tNyK$lJ=1M)H_t;wnW0!=9i_^w-y_59UW<0QRJZ7XEzjb(88|D0)*5>bX3OtcFy0}DORD)%c&sgUS z$b94ni(10is40IB=BW?-fXAy)(^cxyh;(+RU8dWx&ITxlA}lEU_2C<3%$0(LVFH%` zw}^17vC|?Cw2Wf3W?F$i_VU-~HguyX<}zyQjPyqf+cRFpM5Jr548_quBkZ4co5tEL z#6Qq05M#-LQO^^?jRdP%4=L9M(^wn)X*b5-;8k0zN&W3h?dJRHbzA>NMR^5Mw4hTA z1_koO+Vww=elzdhRJqnyL7@?&<$z}EbGjDPWhfXKuyRB)&0Ts$*snTtyjN{04-G(1 zL8${u=%x4#j)U^9F%%{yeg%cfYxOxeu5Z;*Rapvjs{zwso)r1CwyRO z6tdgi<=Yw;T%9mWPT|)r91OL=^I~V04LbXV8!+suP7~GIrE-+A|eo<%3GS+Ue^*B(- zN7WM-oj08MD2bskKI(~NvfJu}+=fAZ2WzqvloJAqCMYJIvqewlI61QvNQ-7E%Uhli zg=jlv%cU&=MH@T~)=f^BpFGThp)fsbGbp;CG^_nK=}XlnD_M%d`ta+ z&ZJOjv8oojFZIS?HJ?XMy2XEHEedfwk({CM z&EsQOX;o5AyI4efdP5p!*Q6Z(_!AEviaRw!J*Ma1Ei(pBdnr#?urM$XGx?mvTNhhT z3H=B4K`rUe9B$YEJn91{hXuBIWv`;lE}8yVGs!8K;3^q_N4CzAQ~E+$3sAN-DLj+E zyVplK<@a@#ma-r6)LK(FC8E-YK}$8d74)MSKGr`LCRR$WomQC|`kWcFWT6}0!Js@Fdg|;=(H62b zt-xSwkvJ$sgeA~*%MVX#?=cdAVs3(UC{aQrUcO?b@V56FO*xiBb0jwIxO@4#2|@yu zG#+_rG~72t%t&Ke&PyzyrK3_rKk zwsuxg#*(@N)u$j@m7WhT-dv1DHgta-sfKO#7tGzOHP3T*NYT8`V61gBZ|gTauD#`1O@*`K2D2R zG~#~HCs3@UYxL(xrqQ{mnVxoyE*dHfj|I)6f3TF&+lKWvY%A%?N`npRf`NDzY}h^g z`_xOr8EK4%v_^7$P{M|1r7TmwL@h@efTs&6w1ZI`cSo{oU~V;r2Yta@$r%)VP{fCa zr>3cB(wf!^D}MN>gr#&e??0^JjJqU_EQeap_Z@W%a%Af>Pp!6xq!+z_2IQmW*5yY_rsep1 zNe6^56dBpSV1{8A^)G!P4W=p=(#hs%4shX}sH?L>4hH|oBKw5BXuI87G+o{i? zmy+8298!RMgzuo2B9m`oZ@*c1HGD2EoqctcZ+1dcXaF1LbuHI z52&AFUycS7i>6YZl}o$jUa4}Z&1iO=p}(6 z{t>PLZlX4JF0*Yl)W$Is*ay4mI!QZ%3<_Sh8Bf24S9F zuC#A?-fzLDjf37fFciiQ0)-M!0lL>!aaLLW(9UEX@*U`Q8qz4bbgOrFWyh>)v^r$1 z(-`5E#OfWeje>;vVPzt7KLN7p_JTi#BC?-XE)?L{Jq{yBnqUx&Rd2TT1NMI z(G5J%jAHM&R#Lki(|EkC_Z~O$D%xGadhXBY!n7&0=8t0TkfsH#!!thEs&(v3BQ4bj zV{k%iX{2x-a&DkdYcI6|%^Va!wZ$rEW0 zA3UIV`FTl{K8zbY@4Gm+EhuCi_-KNWv>el|cE=vgav_USO~F%cYcrI{Hd3B}rYDm0 zaxI_ugF(T5GfBg`QGmGUv82E=v-Ze4xeO1Zn-c~+YB`sn z7G1j4)NBt58xN8|A>X+&GUamL(sd7+JqKovRBlXU#zxsM6V>bc(7E>F7A1@Zw$yUv z#HtRk0b0(%SXz#yS4tdVYa8j@f1N};>*=5caA;P)yA@EE{%{Po{w<=<@4 zLC*8%Jh9?6;0Uw;&3k2c?r>L;ZMy%&-(;=*Z?yjJu4(x_kzY6dn)3Sre>wbq@IO`4 za#t+O_6oo6{C9O*it(Ptg?bIvw5zXj!45r`M)dZ0!V46Nqbm%%w!7`$MGq8qJk10J zisv^21Of`J&Sg!)Q-QX#At0s*69EWm1C?MxMZwT1l5PE zgR&ezp|Mer7FD}hrxQ(yV$Ixmmi-ozk@@CJjh=*H?L3&3QqI_D1B*i1IOL#SpLG0w zM5}8HWIHA17D}O@P%FF$(#zbM>+Hs8z>M2}Mwj`>i|IsUO^mj6@YyG)OZ|bt14l4C z%|ImV$Lw0w}b44@ii9vPu7#Y(;5{XP7{tRYBMB??S)!SSMSF!=gMp=#X|0Jk)}@ z$+4O@)p{4oH8=wbt#&Wlyigu^u3kSmrI4lEo_V}M{flpD_C#94qWn9~{5z*1Fb&Oa z|J>PrhjO$*(df4PS)<8LBT)|P*Q(H*cwV|ac=|DJGL4$lOW^T!L7|o#Kk%N@(;-H* zn+IEySMl%a^6%8Qg*39Yrs~@J6D%6bycgwk1BE=%;>Z3b1J&kavNk{}|6X)IP^q<( zG*)JW%s)i)YqqsrK%tgv()j!IMJbQ5Mh68Wx59e`y!Rx0AdTriLo?P5ZZ)YdYXe9N z2ZikBx9V`SMZ%*eK%p5i#`;)L$VY45DqS{mf6X9P19;V9P-r%&<#^t?bFk5DhQj#h zT2Qn=DHzdy`vO4*`3}uC&}vv21&cgH;RhTxCZ1_$E%hCy2ZJION$~vM#3xy?Dn+!) zVR_OZjcnau)|rH(Be&5UmaK!+9DuB&lQ*x&(Jkj*G8AT2eE}4TE-CpVQ%=oIr@241 zHs-K*K%w@zrMtpJJJS7ujH2j%3Ev*RmC{2iYrl63mZ5$^V zo2+|n0t)RCz;3ukkU`66Y=Z;yYAbwqMTU0!lqllHY~Lj!bBFfT5t}%#^}FA@2?BHYN4g z^iCSA28iSfL7_c?2CW`9D~exb$WqYfwt+%D_}a6unH_8})O z-H}tSvXmK{vbArk&hN}pV4bI|G&OC>!S$;hWHAx8{s$DY&YN1g-kw*#4q$lTJ@5e= zsYOR_AN#JE@6bG!V&37u97o*)f(0TWmiEn`&YFAl@^X}eoK35 zxNe*{UfK#wpBo1^Bv1V1-XYWH_tiWk+&L-B@;Ng${{C5@kH7M6^OwZzvu>3XUr|@gduR#I4p8CR} z@MlAAI((=7c#2}MD9;#-zOWK~h}u-CLuqJI@41s%9$2*8)@J;ABY5h8r{03_W@Wc7 z6tT9(7+j;HwAViqrm1z2Y{_RS(4a0T6kTfG@ARzyl10)d2AHVa0u(J!stgaC^JsZv zT5GcLjX(BUvE{5!sA1DIW8P~y4^K@M)`vfe@k`@xAO7)&Us}0s%8bGMy^(*e%u^0p zn_t@h6c4|@@ysC6uX*Ad>Qk=WnBK@YI$J97_mWwirDLPg<-s}MdzZ*&0f>h@x}m?7 zJM(7zy4;z!g(YrE@cT}=^HHV;^F$g;_IUP63J^D7v7StZdfi zS@-5NccESnN;D`GT|TLw4V}1eJI%Z)ieVPO-?Pe{aWP|-msl7aER0a9TQoK6nrjDb zRyT}|JW&i9BtQc#XmD-IhGBy(S4d~Xj5NLx-G+4Y69z)+r~|zo-yVOEEeAfzKkIyp za%i5oKG#F*Wv1!`mItly4@)TtlZQ{GXf<^568IcxpE zKBy^w3|Z1$I;#mTOZ|HN{yf>8Rq*gffBv~xxpO2%a~HHW|LTLkFZ`Qbg8wkaGh-wF z%9StrTPyLzc+6;co)?1+_}lb++w2WTFfI+@HUF_;*V9dH8!h|5Lm?evR2We>wcE&CkO>(*7F{ z;UCTT*Cqc(1Aaf?uPOhCQSN+{iB)?Xq;sV7nz#MJXYHrwqG>!uU*Pvqehv7?1%6ND zw5?m;;1EG9L1`DryOX&Uk<+~mg`l_ zxXoV+cOK8|-Ilu&XLe5ijRyQPJO0SWV*|7)zXm+}%7}?P(iD!gJkp@S ze^(mM9UExP-y8Y8ihrErw;R9j@a*nmCBbh49v-aC`0K+j?LV)njRFthk4XGBD7T+7 z(Vstx@y`NycIx4Y{9_6KETEU8bgelvU395!hd~(_e`r;WIo}{qXpO$nJ2WJsY)lho zWHP54TH`>W=W4zkv;A^TG=y^}lD{{uA0WMBqjDoCaz%FwdO{!bEm}8j2Ziod44r*F zO@Cy+8lbSxGzMeyT7rka7S@fb z>mmHD%~OuTxiVkRvQRLCf^{%7iAt{^7z-xL22@1!s`_}{;a%^!O$he&t7 zx|^tY`g(k7%=jDblfnyuI_N)t-X-Oq$Cv9NigtC;e|Xjyh@<>t3IDjj|1J-YSN*H_ zW~sn;_g#oSlYP14Lvc< zK9y`cOgf(G?Tl%9#VKx_Tv|J@(tSYLnIm#aH}DYUW8aR+_7^(MlT-SGLK^toet&cB zcaMc~3coad%8238nr_>peen3D=D3R>t0_NEDR^i!+ctX0>^cuq@#ILwJR#0f8%%!T zaUo9od2@R6(UKn0Va}*3-v)2kQ)0~M_(%Nxdf2S=DKC>=;W**LU+C*Z55#%c8JNot zl4<|TK{67tDi zsD)Zax3@m89IjC|k=3F>j!YXmx5k|J*cy75UF;g{6)5Q@eT2-^TilIFG<6B_XTPzB zw6ItHcFiBJr3c@~8JPaPSFpfCD2mFgPGQLLhW= z3#J$O1u_j>g*ZFKQ{v}K1qnpK!2!YI0DqhZ;qN8(6uG$u_)A34kwhR(=<6x;ck@Ly zp}&WS6~Z)vfR1qRa+MzJA`s(&Wt^`PEb+nKbn_Ai28eM;PJq9HDwdNQ z)h#Dgz})(xsw#ijjSsupEDkTopXiK*%0alp`mzFOd?X`_b4B`BalIw;2UFSrhWZHZeBvBdn@WV zoLmsXxk5Fl<0y_GvNLkPD*O6Z(L-K0aLO+PbW{x!dF;9vP%~HDJ!x8b4m|bm`~K6#w5j73!?T45(}8)?4&Ud zK~8{U9sK=8%%p^8_9NzGqv~-k<;^7$%ce9g-XhvN^A`u=^OwxNm{8CNpMajN*#lU%;VCA^vV2BGQaz!(zO6%0J=zMJs6Zdl5)t#8(v3 z0)8?jioJH1QK)LXWAT zfGu7q3Gfs9(Hpiuje!IDFnL5o!dx>rAW#%6iIAF&PS(IdCcY5@PmwRu#R|;E!vURn zDDpN^AoZ6@K-MqUWWowv|EF_;`{#GLvl!|oW_y{C$uKr4gpRNYppWo#+a6?Z++>|;ae<$N+gCOpuT$X~ip_?0| z06vhP788p5k#u8LT3vb_fw|jV0gpU%fKPtG=vPs{{UwM{?1P9u(ilk{W5Jq(f_wpFWVk z;?1}^A_HA>6&dg~F(!DO}{+0qO%Eh9l@(b();czJGHzY^8&!o8fu^U%35;2=n z(zB&Vf*-bmFeJEo1_aWeOE;>YC-iq0o67glP&k>!T_~1d*vCN^qM#6=FSC;*^cA=U_)$SJ z*;2W|uJrR&0u&EFa>dI%Kk3rVH-Qfp4bm!wdSSl_uK>{tUos(BSGaq52$*M|r3!_4 z2GHJ_7&~qd$*%XH7Pczcj`$ldI{{%+NlU*Fe=kV{ZP{UqF7QtRtB=%G7%TiN)Gxp- z1l95K3k(RxE~z)|#bKw$1!+|`Zt-wA0iXWcySNWJOR8cVg z&4|C%2Sxps@^?a6i^*-vH6es@FA=fWF++MT3Dd*qv;?NF*>zzK!c$-~Ru^CTkAmJ4f=-zn~{zYn>P@Ou(wN$k9l&YY8u z%nE8E&qT6*axk5}LT#jn+Odo{-VXj>@vbKwk_Q(0Kq+{$o9fI2t_pY7IJqE!bA^H{ zc4wLMNL@166(VF`<%&xu00L%Po5EipGuShe3ruwst)t4xjS|X9WugRQTbd)XC+R7k zB(j;9N(&-{e!etxRp>3UWA(r$`^v1dDr#4*4ZzF2WDLddOQ)U!>8yf1U<76h@KfMy z+-wlcz5J`u<(D!c;FoLpgoYO8m~rAaF!*;He&&IgpWm65si^--kAxz-FlW|c#!n|X zD(<)J;fr8mzX_ygFIK=NJueqr(k}vY8bKNTn6n4rn&Aqn_%ja#{QNFgx4cOj)yn{OaCm1VP#82McA7M3QFZ{Hh`CNMRr6*6gv`U*8<#} zE5w9~@AqSOj992#={IK7U^)sCMBx&mD$@ODCaDOPg9!o^bpg)S3z#`q%tDI^W_WJL z1&`-2lLb7jqd<}0azfy5Da<&{G@^{atXcm;B$JOLkhw-cVJC$OB*@NK1+(lc6Nnhg zFddFLpVh>a#s~Se0*Oq1h+{sPDpk}?n3Gw-!F8i6`R{qHwxrt3Vbf7vyoSn4Vfu zRN@o|Zq5}G9V+6MopnnEv&Y`bYobVJ=}EX!HaZhmJ`Ie(;ei1H`~X3SqEUsD3qm+o zOq*4tHv8x^IN5KEXH_)%vphI`Gl1QY(G$^Jq=MSdX9^ivRVpk*o^82{$Z1*CHBG_bQL`ArdtmwyBe zfOrN(jC=H60)@9k$|;b$$65{@-0FwqCXJPkdHr;tB)Tfy}6J4QOZ?OOro zZyT%N|1E{-@6w%COmWz!<`v#Nm*#;a=@&#j+Y0Bv<+v!jI*{&ULfS9a95*P3mDzpd z>ygs+1@86Pa967(JK!&2*k=V3mRQlIa<+X-L`NhAM z#~2Wzn9skH^7}pzjlPTr`g*yT(r>s3Tz$Pv@Xmk&dr>x&%H*aUk%|@}6yEW(w_stv z32^VI;x*f^?!>(B01@S+!hbDs^e*)y>8%zoe^=TUm!45&f`}}AB-(^#ioUp8%;e&x zV@nt}Bz{kp1c>RjDm`Ux!WELsd(-P2aXr?8k_hn4zCbO4F z%wEwZ)(p#C#zwMcSJDAuiXjUG{usIKf*#lt$->*-Sq0%s!&;@LGM@2!2oLo%H{^X&> z2D>jLZBgkayMP`P75Iw$JtUq?x-^+%CEU7q6$wLyUcSstaOz`t@{f)x!;{$b03|%Y zO(+oxLcK)zjY{UBSkARv{BMZ??(TT39s1&50sMk#1UwUt6il@PgJ}Zp6)KWF)yL6@ z=2G-{ne<`CpYOKd_L{UaNIdc83}%$%2lT+=&m4IBg-ypJ`FO2{-hmQ`eeswPY)ds_ z)yE}Ojp~c9KlR3UJYs~GY(hnXNuuBYTti;4%p7uNXg$3Z^M= z8JQb`m2*XXxxyRMKRYLQe|~55W%gLfQDl9K?f(i4QBH1%Ehm*xjWGxtz?j}6>xl|V z;Onp`hL7}D#xG9|$Sf}z@+w}Rl^+Qqv3vrfBeOM)m+iz}^cWGxsRb0u6wM3xnNT7> zk*P^V^XK1s90dNB!jXc0;NpchDJ5cQ$3kC4*!vlz6|`%)`B7H6=}d{t+y~w+Es(wh z1o(=C{_OM3NbnEwbHRhe(&&k6=|G@BiJy5;@Xzn^!W3}+7GA;sTMEZW{M(2qQ??x| z`wG5_Z}XF`%(%$v&3ykWy}|j5m81fGN%~tkznWhv=+`u+E)_*VXe4b>Rs!Qrj1Cn! zD)oQX`@qY&`YUd3cash^5b|dtWYI&jnDAnV_QfkICYWL{BPrz#a97|X@{WVqyU^Eb5~ef( zH2q-SZ$>5$kv}^(myHn8Vz96E|2207yLE&x@QYp)_15ocDnyf7HUVz3R*B(siVI%hqYY^5myju%uG$8zEcDrF&pmIjVLV9CnR4@`LRkiU2zq3K>9M9hDN}+rh4ps=nP_q;`>%TUK>1k+V^hr-&NUzTb z(4GL^v_~&L?c5ngh-aBykJtdIt*3>zBkhsc_2?SVOdJDmHA=gO8Nj_6ZvPEXrXjTj>-&6KcTv57lBylQ@Em4DZZhQgU z`~@&0VAYzPlOAWs#Z*4z4Y76Qw-zM#_}L@?H6>lNyjLkmvnl$#pYr;>*r zo6?D>#zHBXI_yl^!r}0l8`UAe#ap7QFQ*%vMNubo7-y?Rc8lvlho!Fsz?bjn<@Ez9 z%SrGRp%{kq%O2qBjj&UTCW}b$Me)di@)9OfYTnf%w z#63@cUlD!z;;|=-Iz>rqOGeAgu_<%)RrK5fv?th?r5%H<@c?KEGN7h*v(j>vYktL+ zxcQHVnrMID?;m#5^A8R^#tPb1$3l2{XD%@F@n#Lt!Ny$;DBh*1tkkqn`2|2tLLw0o z;XejXZ=O*e5ia+6e_?Ro<;`zL9-to29RI49L*u%q@@Uk9r@>U%ZWaO!gyQ{dFoj5s zsn)=O6<*Ya69n*M=l|FHkIqp`c@)PjG-fzDI8~JmsGTudv~rdm;4?C~jMGNuO?-NO zy?wn|bbt*7ZqzWLQVgnNeJV~rd9r#-0%Hl4C{cGh>e4Ol9G$5gC~iRuH+qxo zLZ}JTG6Q}jvb}1V^D}-=83_5frTou5h{Xd#0#ag)w}*U1ezsI$DQx4*4RGfg;|);t zA+@1w#5+v5%}@yTjgp=R$S`^uPz<8}c3d8bds3c%Jo{9<=*^l9AT^2j4ZD<8e7V8I z>>A5QKb8|Y%9UV22?fe4Cwmwy6LBRd{1$c_CH3#3392ZX72J)mi0KVmTA;k=U7;V? zst2uJl&JZrxlwgZvR9P`jY_S46t{mYrqtBIKMLhdT$_DYh0Q99?2ijg0^iynw{dAv zl+3^_De9HY;a$cJKuQW?!C4zd%?ESXzrVq^pc#vdz>6o$tcdninl7rrHL}LZ*Bdy% bUAp#xSi)>pD^+piiNeg^Ab3gcfB5Gg-t0)x literal 0 HcmV?d00001 diff --git a/app/server/services/user/eslint.config.ts b/app/server/services/user/eslint.config.ts new file mode 100644 index 0000000..673b075 --- /dev/null +++ b/app/server/services/user/eslint.config.ts @@ -0,0 +1,11 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginReact from "eslint-plugin-react"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } }, + tseslint.configs.recommended, + pluginReact.configs.flat.recommended, +]); diff --git a/app/server/user/package.json b/app/server/services/user/package.json similarity index 56% rename from app/server/user/package.json rename to app/server/services/user/package.json index 6c840b5..a236d31 100644 --- a/app/server/user/package.json +++ b/app/server/services/user/package.json @@ -4,9 +4,17 @@ "type": "module", "private": true, "devDependencies": { + "@eslint/js": "^9.39.2", "@types/bun": "latest", "@types/express": "^5.0.6", - "@types/node": "^25.1.0" + "@types/node": "^25.1.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "globals": "^17.3.0", + "jiti": "^2.6.1", + "typescript-eslint": "^8.54.0" }, "peerDependencies": { "typescript": "^5.9.3" diff --git a/app/server/user/src/index.ts b/app/server/services/user/src/index.ts similarity index 100% rename from app/server/user/src/index.ts rename to app/server/services/user/src/index.ts diff --git a/app/server/user/tsconfig.json b/app/server/services/user/tsconfig.json similarity index 100% rename from app/server/user/tsconfig.json rename to app/server/services/user/tsconfig.json From f18cca6c7a15569131079c9f53ea36117564a480 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 6 Feb 2026 14:56:52 -0600 Subject: [PATCH 016/204] removed user auth folder --- user-auth/client/.gitignore | 24 - user-auth/client/README.md | 16 - user-auth/client/bun.lock | 451 ---- user-auth/client/eslint.config.js | 29 - user-auth/client/index.html | 13 - user-auth/client/package-lock.json | 2926 ------------------------- user-auth/client/package.json | 31 - user-auth/client/public/vite.svg | 1 - user-auth/client/src/App.css | 42 - user-auth/client/src/App.jsx | 233 -- user-auth/client/src/api.js | 19 - user-auth/client/src/assets/react.svg | 1 - user-auth/client/src/index.css | 347 --- user-auth/client/src/main.jsx | 10 - user-auth/client/tsconfig.json | 29 - user-auth/client/vite.config.js | 7 - user-auth/server/app.db | Bin 16384 -> 0 bytes user-auth/server/db.ts | 21 - user-auth/server/index.ts | 113 - user-auth/server/schema.sql | 6 - 20 files changed, 4319 deletions(-) delete mode 100644 user-auth/client/.gitignore delete mode 100644 user-auth/client/README.md delete mode 100644 user-auth/client/bun.lock delete mode 100644 user-auth/client/eslint.config.js delete mode 100644 user-auth/client/index.html delete mode 100644 user-auth/client/package-lock.json delete mode 100644 user-auth/client/package.json delete mode 100644 user-auth/client/public/vite.svg delete mode 100644 user-auth/client/src/App.css delete mode 100644 user-auth/client/src/App.jsx delete mode 100644 user-auth/client/src/api.js delete mode 100644 user-auth/client/src/assets/react.svg delete mode 100644 user-auth/client/src/index.css delete mode 100644 user-auth/client/src/main.jsx delete mode 100644 user-auth/client/tsconfig.json delete mode 100644 user-auth/client/vite.config.js delete mode 100644 user-auth/server/app.db delete mode 100644 user-auth/server/db.ts delete mode 100644 user-auth/server/index.ts delete mode 100644 user-auth/server/schema.sql diff --git a/user-auth/client/.gitignore b/user-auth/client/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/user-auth/client/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/user-auth/client/README.md b/user-auth/client/README.md deleted file mode 100644 index 18bc70e..0000000 --- a/user-auth/client/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/user-auth/client/bun.lock b/user-auth/client/bun.lock deleted file mode 100644 index 9a24b35..0000000 --- a/user-auth/client/bun.lock +++ /dev/null @@ -1,451 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 0, - "workspaces": { - "": { - "name": "client", - "dependencies": { - "react": "^19.2.0", - "react-dom": "^19.2.0", - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/bun": "latest", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "vite": "^7.2.4", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - - "@babel/generator": ["@babel/generator@7.29.0", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - - "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], - - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], - - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.3", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.2", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg=="], - - "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": "dist/cli.js" }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], - - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="], - - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": "bin/esbuild" }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": "bin/eslint.js" }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], - - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], - - "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - - "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], - - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - } -} diff --git a/user-auth/client/eslint.config.js b/user-auth/client/eslint.config.js deleted file mode 100644 index 4fa125d..0000000 --- a/user-auth/client/eslint.config.js +++ /dev/null @@ -1,29 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{js,jsx}'], - extends: [ - js.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', - }, - }, - rules: { - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], - }, - }, -]) diff --git a/user-auth/client/index.html b/user-auth/client/index.html deleted file mode 100644 index f07b3d0..0000000 --- a/user-auth/client/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - client - - -

- - - diff --git a/user-auth/client/package-lock.json b/user-auth/client/package-lock.json deleted file mode 100644 index 611588a..0000000 --- a/user-auth/client/package-lock.json +++ /dev/null @@ -1,2926 +0,0 @@ -{ - "name": "client", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "client", - "version": "0.0.0", - "dependencies": { - "react": "^19.2.0", - "react-dom": "^19.2.0" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "vite": "^7.2.4" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", - "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", - "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.2", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001767", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", - "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.283", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", - "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/user-auth/client/package.json b/user-auth/client/package.json deleted file mode 100644 index 214eeec..0000000 --- a/user-auth/client/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "client", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "react": "^19.2.0", - "react-dom": "^19.2.0" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "vite": "^7.2.4", - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } -} diff --git a/user-auth/client/public/vite.svg b/user-auth/client/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/user-auth/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/user-auth/client/src/App.css b/user-auth/client/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/user-auth/client/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/user-auth/client/src/App.jsx b/user-auth/client/src/App.jsx deleted file mode 100644 index 38840c1..0000000 --- a/user-auth/client/src/App.jsx +++ /dev/null @@ -1,233 +0,0 @@ -import { useMemo, useState } from "react"; -import { signup, login } from "./api"; - -function IconMail(props) { - return ( - - - - - ); -} - -function IconLock(props) { - return ( - - - - - - ); -} - -function IconEye({ open = false, ...props }) { - return open ? ( - - - - - ) : ( - - - - - - - ); -} - -export default function App() { - const [mode, setMode] = useState("login"); // login | signup - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [showPw, setShowPw] = useState(false); - - const [status, setStatus] = useState(null); // {type, message} - const [user, setUser] = useState(null); - const [busy, setBusy] = useState(false); - - const headline = useMemo(() => { - return mode === "login" ? "Welcome back" : "Create your account"; - }, [mode]); - - async function handleSubmit(e) { - e.preventDefault(); - setStatus(null); - setBusy(true); - - try { - const action = mode === "signup" ? signup : login; - const result = await action(email, password); - - if (!result.ok) { - setUser(null); - setStatus({ type: "error", message: result.error || "Something went wrong." }); - return; - } - - setStatus({ type: "success", message: result.message || "Success!" }); - if (result.user) setUser(result.user); - } finally { - setBusy(false); - } - } - - function switchMode() { - setStatus(null); - setUser(null); - setPassword(""); - setShowPw(false); - setMode((m) => (m === "login" ? "signup" : "login")); - } - - return ( -
- {/* animated background */} -
-
-
- -
-
-
- -
-
-
F
-
-

Finus

-

{headline}

-
-
- -
- - {mode === "login" ? "Secure Sign-In" : "New Account"} -
-
- -
-
- -
- - setEmail(e.target.value)} - autoComplete="email" - required - /> -
-
- -
- -
- - setPassword(e.target.value)} - autoComplete={mode === "login" ? "current-password" : "new-password"} - required - /> - -
-
- - -
- - {status && ( -
- {status.message} -
- )} - - {user && ( -
- Aprroved: Signed in as {user.email} -
- )} - -
- -

switch

- -
- - -
-
-
- ); -} diff --git a/user-auth/client/src/api.js b/user-auth/client/src/api.js deleted file mode 100644 index 9785ac4..0000000 --- a/user-auth/client/src/api.js +++ /dev/null @@ -1,19 +0,0 @@ -const API_BASE = "http://localhost:4000"; - -export async function signup(email, password) { - const res = await fetch(`${API_BASE}/api/auth/signup`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }), - }); - return await res.json(); -} - -export async function login(email, password) { - const res = await fetch(`${API_BASE}/api/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }), - }); - return await res.json(); -} diff --git a/user-auth/client/src/assets/react.svg b/user-auth/client/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/user-auth/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/user-auth/client/src/index.css b/user-auth/client/src/index.css deleted file mode 100644 index 6bd408c..0000000 --- a/user-auth/client/src/index.css +++ /dev/null @@ -1,347 +0,0 @@ -/* Full-screen app */ -html, body, #root { - height: 100%; - width: 100%; - margin: 0; -} - -:root { - --bg: #0b0f0c; - --card: rgba(15, 26, 18, 0.92); - --border: rgba(42, 90, 61, 0.85); - --text: #eaffef; - --muted: #bfeccc; - --green: #2cff79; - --green2: #7cff9b; -} - -body { - background: var(--bg); - color: var(--text); - font-family: system-ui, Arial, sans-serif; - overflow: hidden; -} - -/* Layout */ -.page { - position: relative; - min-height: 100%; - display: flex; - align-items: center; - justify-content: center; - padding: 28px; -} - -/* Animated background grid */ -.bg-grid { - position: absolute; - inset: 0; - background-image: - linear-gradient(rgba(44, 255, 121, 0.06) 1px, transparent 1px), - linear-gradient(90deg, rgba(44, 255, 121, 0.06) 1px, transparent 1px); - background-size: 48px 48px; - mask-image: radial-gradient(circle at 50% 45%, black 0%, transparent 70%); - opacity: 0.9; -} - -/* Floating orbs */ -.orb { - position: absolute; - width: 520px; - height: 520px; - border-radius: 50%; - filter: blur(18px); - opacity: 0.18; - animation: float 10s ease-in-out infinite; - pointer-events: none; -} - -.orb-1 { - background: radial-gradient(circle, rgba(44,255,121,0.85), rgba(0,0,0,0) 60%); - top: 10%; - left: 10%; -} - -.orb-2 { - background: radial-gradient(circle, rgba(124,255,155,0.7), rgba(0,0,0,0) 60%); - bottom: 8%; - right: 10%; - animation-duration: 12s; -} - -@keyframes float { - 0%, 100% { transform: translateY(0px) translateX(0px); } - 50% { transform: translateY(-18px) translateX(12px); } -} - -.wrap { - width: 100%; - display: flex; - justify-content: center; - z-index: 1; -} - -/* Card */ -.card { - position: relative; - width: 100%; - max-width: 470px; - background: var(--card); - border-radius: 26px; - padding: 22px; - box-shadow: 0 18px 60px rgba(0,0,0,0.55); - backdrop-filter: blur(10px); - overflow: hidden; -} - -/* Gradient border ring */ -.card-border { - position: absolute; - inset: 0; - padding: 1px; - border-radius: 26px; - background: linear-gradient( - 135deg, - rgba(44,255,121,0.55), - rgba(31,58,42,0.3), - rgba(124,255,155,0.45) - ); - -webkit-mask: - linear-gradient(#000 0 0) content-box, - linear-gradient(#000 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; - pointer-events: none; -} - -/* Top area */ -.top { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 18px; -} - -.brand { - display: flex; - align-items: center; - gap: 12px; -} - -.logo { - width: 48px; - height: 48px; - border-radius: 16px; - display: grid; - place-items: center; - background: var(--green); - color: #06210f; - font-weight: 900; - font-size: 18px; - box-shadow: 0 12px 30px rgba(44,255,121,0.16); -} - -.brand-title { - margin: 0; - font-size: 28px; - line-height: 1.1; - color: var(--green2); -} - -.brand-sub { - margin: 4px 0 0; - font-size: 13px; - color: var(--muted); -} - -.pill { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - color: var(--muted); - border: 1px solid rgba(31, 58, 42, 0.95); - background: rgba(11, 18, 13, 0.55); - padding: 8px 10px; - border-radius: 999px; - user-select: none; -} - -.dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--green); - box-shadow: 0 0 0 6px rgba(44,255,121,0.12); -} - -/* Form */ -.form { - display: grid; - gap: 14px; -} - -.field { - display: grid; - gap: 7px; -} - -.label { - font-size: 13px; - color: #d7ffe2; -} - -.input-shell { - display: flex; - align-items: center; - gap: 8px; - border-radius: 16px; - border: 1px solid var(--border); - background: rgba(11, 18, 13, 0.9); - padding: 10px 12px; - transition: transform 0.12s ease, border-color 0.12s ease; -} - -.input-shell:focus-within { - border-color: rgba(44,255,121,0.75); - transform: translateY(-1px); -} - -.icon { - display: inline-flex; - color: rgba(234, 255, 239, 0.78); -} - -.input { - width: 100%; - border: none; - outline: none; - background: transparent; - color: var(--text); - font-size: 14px; -} - -.icon-btn { - border: none; - background: transparent; - color: rgba(234, 255, 239, 0.78); - cursor: pointer; - padding: 6px; - border-radius: 10px; -} - -.icon-btn:hover { - background: rgba(44,255,121,0.08); -} - -/* Primary button */ -.btn { - position: relative; - margin-top: 6px; - width: 100%; - padding: 12px 14px; - border-radius: 16px; - border: none; - cursor: pointer; - font-weight: 900; - color: #06210f; - background: linear-gradient(180deg, rgba(44,255,121,1), rgba(124,255,155,0.95)); - transition: transform 0.12s ease, filter 0.12s ease; - overflow: hidden; -} - -.btn:hover { transform: translateY(-1px); filter: brightness(1.02); } -.btn:active { transform: translateY(0px) scale(0.99); } -.btn:disabled { opacity: 0.7; cursor: not-allowed; } - -.btn-glow { - position: absolute; - inset: -40px; - background: radial-gradient(circle, rgba(255,255,255,0.25), rgba(255,255,255,0) 55%); - transform: translateX(-55%); - animation: sweep 2.8s ease-in-out infinite; - pointer-events: none; -} - -@keyframes sweep { - 0%, 100% { transform: translateX(-55%); } - 50% { transform: translateX(55%); } -} - -/* Messages */ -.msg { - margin-top: 10px; - padding: 10px 12px; - border-radius: 16px; - font-size: 14px; -} - -.msg-err { - background: rgba(42, 15, 18, 0.9); - border: 1px solid #6d1f2a; - color: #ffd2d7; -} - -.msg-ok { - background: rgba(13, 36, 20, 0.85); - border: 1px solid #1f6d3a; - color: #c8ffd6; -} - -.userbox { - margin-top: 10px; - padding: 10px 12px; - border-radius: 16px; - border: 1px dashed rgba(44,255,121,0.85); -} - -/* Divider + switch */ -.divider { - margin: 18px 0 10px; - display: flex; - align-items: center; - gap: 10px; - opacity: 0.85; -} - -.divider span { - height: 1px; - background: rgba(31,58,42,0.95); - flex: 1; -} - -.divider p { - margin: 0; - font-size: 12px; - color: var(--muted); - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.ghost { - width: 100%; - padding: 12px 14px; - border-radius: 16px; - border: 1px solid rgba(42, 90, 61, 0.9); - background: rgba(11, 18, 13, 0.35); - color: var(--muted); - cursor: pointer; - font-weight: 800; - transition: transform 0.12s ease, border-color 0.12s ease; -} - -.ghost:hover { - transform: translateY(-1px); - border-color: rgba(44,255,121,0.6); -} - -.foot { - margin-top: 14px; - font-size: 12px; - color: rgba(191, 236, 204, 0.85); -} - -.foot-muted { - color: rgba(191, 236, 204, 0.65); -} diff --git a/user-auth/client/src/main.jsx b/user-auth/client/src/main.jsx deleted file mode 100644 index b9a1a6d..0000000 --- a/user-auth/client/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' - -createRoot(document.getElementById('root')).render( - - - , -) diff --git a/user-auth/client/tsconfig.json b/user-auth/client/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/user-auth/client/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/user-auth/client/vite.config.js b/user-auth/client/vite.config.js deleted file mode 100644 index 8b0f57b..0000000 --- a/user-auth/client/vite.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}) diff --git a/user-auth/server/app.db b/user-auth/server/app.db deleted file mode 100644 index 421f2710047d8f8c028b0db5657ac0ef1001d5b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI(J#X4T7zgk(L}`LVXeUpVZa@kaGy;PmL_!K}+<;(5r zze&GJze>lb>QMD?n@S+HL&xa<=+5W!y-3e*^I3j%sW~B|Z~eJzhO|r`6G0%SloCP` zyzcON9|FJG?%$97txb@lPv6s-ZzOr}nH>DceBliO0uX=z1Rwwb2tWV=5P$##An;Ft zesWjL=Y?Ttj%O^mnei9bQNV5@=1sP{J1+)mQ&*|pe6FcLrIjW;5=Bw? zxQO?R0yYogOJZYv{D|h#DM}rSw!6A|t`2B_(C#z`LwcbOY17bq?Ji%`QM-C!J(0Pl zGo!ltMyK5#uZE^kquahT)Oh&X41$F}w?K&|2t_j3jMf0inAD%;wH(V_tw~HUWLpR;ROK! z2tWV=5P$##AOHafKmY;|fWUtWtey(HL|QG&Aq&D&dwnQyH1XZ6Ik$aJcC74eqg2nj zjjE!QtA(&37p@zn?1U{VooTpWl}fLs)oTO$R;?TF%<|G3=8xyr z^Of*|fB*y_009U<00Izz00bZa0SG|gK?poaO8c7!5x>6w-;vDSgGdlbKmY;|fB*y_ a009U<00Izz00bcLM_^A#iu>`806zh0DzM4` diff --git a/user-auth/server/db.ts b/user-auth/server/db.ts deleted file mode 100644 index c31ca83..0000000 --- a/user-auth/server/db.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Database } from "bun:sqlite"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; - -export const db = new Database("app.db"); - -// Run schema on startup -const schemaPath = join(import.meta.dir, "schema.sql"); -const schema = readFileSync(schemaPath, "utf-8"); -db.exec(schema); - -// Prepared statements -export const stmts = { - findByEmail: db.prepare( - "SELECT id, email, password_hash FROM users WHERE email = ?" - ), - - createUser: db.prepare( - "INSERT INTO users (email, password_hash) VALUES (?, ?)" - ), -}; diff --git a/user-auth/server/index.ts b/user-auth/server/index.ts deleted file mode 100644 index 3c87275..0000000 --- a/user-auth/server/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { stmts } from "./db"; - -// ---- helpers ---- -function json(body: unknown, status = 200) { - return new Response(JSON.stringify(body), { - status, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "http://localhost:5173", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - }); -} - -function badRequest(message: string) { - return json({ ok: false, error: message }, 400); -} - -function isValidEmail(email: string) { - // Simple, practical email check (good enough for most apps) - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); -} - -async function hashPassword(password: string) { - return await Bun.password.hash(password); -} - -async function verifyPassword(password: string, hash: string) { - return await Bun.password.verify(password, hash); -} - -// ---- server ---- -Bun.serve({ - port: 4000, - async fetch(req) { - const url = new URL(req.url); - - if (req.method === "OPTIONS") { - return new Response(null, { - status: 204, - headers: { - "Access-Control-Allow-Origin": "http://localhost:5173", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - }); - } - - // ---- SIGNUP ---- - if (req.method === "POST" && url.pathname === "/api/auth/signup") { - const body = (await req.json().catch(() => null)) as null | { - email?: string; - password?: string; - }; - - if (!body?.email || !body?.password) { - return badRequest("Email and password are required."); - } - - const email = body.email.trim().toLowerCase(); - const password = body.password; - - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); - if (password.length < 8) return badRequest("Password must be at least 8 characters."); - - const existing = stmts.findByEmail.get(email) as any; - if (existing) return json({ ok: false, error: "Email already exists." }, 409); - - const passwordHash = await hashPassword(password); - stmts.createUser.run(email, passwordHash); - - return json({ ok: true, message: "Account created." }, 201); - } - - // ---- LOGIN ---- - if (req.method === "POST" && url.pathname === "/api/auth/login") { - const body = (await req.json().catch(() => null)) as null | { - email?: string; - password?: string; - }; - - if (!body?.email || !body?.password) { - return badRequest("Email and password are required."); - } - - const email = body.email.trim().toLowerCase(); - const password = body.password; - - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); - - const user = stmts.findByEmail.get(email) as any; - if (!user) return json({ ok: false, error: "Invalid email or password." }, 401); - - const valid = await verifyPassword(password, user.password_hash); - if (!valid) return json({ ok: false, error: "Invalid email or password." }, 401); - - return json({ - ok: true, - message: "Logged in!", - user: { id: user.id, email: user.email }, - }); - } - - if (req.method === "GET" && url.pathname === "/api/health") { - return json({ ok: true, status: "up" }); - } - - return json({ ok: false, error: "Not found" }, 404); - }, -}); - -console.log("API running on http://localhost:4000"); diff --git a/user-auth/server/schema.sql b/user-auth/server/schema.sql deleted file mode 100644 index 9292381..0000000 --- a/user-auth/server/schema.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - email TEXT NOT NULL UNIQUE, - password_hash TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); From 3538e6868038764334bdc0fa62777cab09e1f9f6 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 6 Feb 2026 16:19:23 -0600 Subject: [PATCH 017/204] whoops. here's the schema --- app/server/database/schema.sql | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/server/database/schema.sql diff --git a/app/server/database/schema.sql b/app/server/database/schema.sql new file mode 100644 index 0000000..90a1d38 --- /dev/null +++ b/app/server/database/schema.sql @@ -0,0 +1,20 @@ +CREATE DATABASE finus; + +CREATE TABLE finus.account ( + id INTEGER NOT NULL AUTO_INCREMENT, + username VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + age INTEGER NOT NULL, + created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE finus.credentials ( + account_id INTEGER NOT NULL, + pw_hash BLOB(256) NOT NULL, + salt CHAR(8) NOT NULL, + PRIMARY KEY (account_id), + FOREIGN KEY (account_id) REFERENCES finus.account(id) ON DELETE CASCADE +); From 6dab410b92749a742e91e1c9b904d8c814792c01 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Fri, 6 Feb 2026 18:01:11 -0600 Subject: [PATCH 018/204] Client: Added layout, dashboard and header component for setup --- app/client/src/App.tsx | 13 +++++++------ app/client/src/components/Header.tsx | 9 +++++++++ app/client/src/components/Layout.tsx | 14 ++++++++++++++ app/client/src/pages/Dashboard.tsx | 0 app/client/src/pages/DashboardPage.tsx | 9 +++++++++ 5 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 app/client/src/components/Header.tsx create mode 100644 app/client/src/components/Layout.tsx delete mode 100644 app/client/src/pages/Dashboard.tsx create mode 100644 app/client/src/pages/DashboardPage.tsx diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 3f18cbb..6ba05b8 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -6,6 +6,9 @@ import { Suspense, useState, lazy} from 'react' import './App.css' import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom' import LoadingSpinner from './components/LoadingSpinner.tsx' +import DashboardPage from './pages/DashboardPage.tsx' +import Layout from './components/Layout.tsx' + function App() { return ( @@ -21,19 +24,17 @@ function Main() { const location = useLocation() return ( -
- {/* Conditionally render Navbar - adjust logic as needed */} - {location.pathname !== '/' && location.pathname !== '/login'} - }> {/* } /> } /> */} {/* Add a 404 page */} - } /> + }> + } /> + + {/*} />*/} -
) } diff --git a/app/client/src/components/Header.tsx b/app/client/src/components/Header.tsx new file mode 100644 index 0000000..79922da --- /dev/null +++ b/app/client/src/components/Header.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function Header() { + return ( +
Header
+ ) +} + +export default Header \ No newline at end of file diff --git a/app/client/src/components/Layout.tsx b/app/client/src/components/Layout.tsx new file mode 100644 index 0000000..235d5d0 --- /dev/null +++ b/app/client/src/components/Layout.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { Outlet, useLocation } from 'react-router-dom' + +function Layout() { + const location = useLocation() + return ( + <> +
From Layout - current path: {location.pathname}
+ + + ) +} + +export default Layout \ No newline at end of file diff --git a/app/client/src/pages/Dashboard.tsx b/app/client/src/pages/Dashboard.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..667fc1f --- /dev/null +++ b/app/client/src/pages/DashboardPage.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function DashboardPage() { + return ( +
From DashboardPage
+ ) +} + +export default DashboardPage \ No newline at end of file From b4c0cedf698448edd66cf5564f92bd610c1b1d48 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 6 Feb 2026 18:06:19 -0600 Subject: [PATCH 019/204] finished schema --- app/server/database/schema.sql | 115 ++++++++++++++++++++++++++-- app/server/database/updateSchema.sh | 4 + 2 files changed, 112 insertions(+), 7 deletions(-) create mode 100755 app/server/database/updateSchema.sh diff --git a/app/server/database/schema.sql b/app/server/database/schema.sql index 90a1d38..f72bc55 100644 --- a/app/server/database/schema.sql +++ b/app/server/database/schema.sql @@ -1,7 +1,7 @@ CREATE DATABASE finus; -CREATE TABLE finus.account ( - id INTEGER NOT NULL AUTO_INCREMENT, +CREATE TABLE finus.finusAccount ( + id INTEGER NOT NULL AUTO_INCREMENT, username VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL, @@ -12,9 +12,110 @@ CREATE TABLE finus.account ( ); CREATE TABLE finus.credentials ( - account_id INTEGER NOT NULL, - pw_hash BLOB(256) NOT NULL, - salt CHAR(8) NOT NULL, - PRIMARY KEY (account_id), - FOREIGN KEY (account_id) REFERENCES finus.account(id) ON DELETE CASCADE + finus_account_id INTEGER NOT NULL AUTO_INCREMENT, + pw_hash BLOB(256) NOT NULL, + salt CHAR(8) NOT NULL, + PRIMARY KEY (finus_account_id), + FOREIGN KEY (finus_account_id) REFERENCES finus.finusAccount(id) ON DELETE CASCADE ); + +CREATE TABLE finus.profile ( + id INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + PRIMARY KEY (id) +); + +CREATE TABLE finus.goalType ( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + +CREATE TABLE finus.goal ( + id INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + type VARCHAR(50) NOT NULL, + objective VARCHAR(500) NOT NULL, + description VARCHAR(500), + deadline DATETIME, + PRIMARY KEY (id), + FOREIGN KEY (type) REFERENCES finus.goalType(type) ON DELETE CASCADE + +); + +CREATE TABLE finus.profile_goal ( + profile_id INTEGER NOT NULL, + goal_id INTEGER NOT NULL, + PRIMARY KEY (profile_id, goal_id), + FOREIGN KEY (profile_id) REFERENCES finus.profile(id), + FOREIGN KEY (goal_id) REFERENCES finus.goal(id) +); + +CREATE TABLE finus.financialAccountType ( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + +CREATE TABLE finus.financialAccountSubtype( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + +CREATE TABLE finus.financialAccount ( + id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + type VARCHAR(50) NOT NULL, + balance INTEGER NOT NULL, + value INTEGER NOT NULL, + last_updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + subtype VARCHAR(50), + PRIMARY KEY (id), + FOREIGN KEY (type) REFERENCES finus.financialAccountType(type) ON DELETE CASCADE, + FOREIGN KEY (subtype) REFERENCES finus.financialAccountType(type) ON DELETE CASCADE + +); + +CREATE TABLE finus.finusAccount_profile ( + profile_id INTEGER NOT NULL, + account_id INTEGER NOT NULL, + PRIMARY KEY (profile_id, account_id), + FOREIGN KEY (profile_id) REFERENCES finus.profile(id), + FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE +); + +CREATE TABLE finus.transaction ( + id INTEGER NOT NULL AUTO_INCREMENT, + financialAccount_id INTEGER NOT NULL, + amount INTEGER NOT NULL, + description VARCHAR(500), + sender VARCHAR(50), + recipient VARCHAR(50), + date DATETIME, + PRIMARY KEY (id), + FOREIGN KEY (financialAccount_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE +); + +CREATE TABLE finus.asset ( + id INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + fixed_compound_rate DOUBLE, + PRIMARY KEY (id) +); + +CREATE TABLE finus.investment ( + account_id INTEGER NOT NULL, + asset_id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + quantity INTEGER NOT NULL DEFAULT 1, + cost INTEGER NOT NULL, + last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (account_id, asset_id), + FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE, + FOREIGN KEY (asset_id) REFERENCES finus.asset(id) ON DELETE CASCADE +); + + +#Populate lookup tables +#INSERT INTO finus.financialAccountType (type) VALUES (); +#INSERT INTO finus.financialAccountSubtype (type) VALUES (); +INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); diff --git a/app/server/database/updateSchema.sh b/app/server/database/updateSchema.sh new file mode 100755 index 0000000..947919f --- /dev/null +++ b/app/server/database/updateSchema.sh @@ -0,0 +1,4 @@ +echo "Wipe database:" +mysql -c -h 127.0.0.1 -P 3306 -u root -p -e "DROP DATABASE finus"; +echo "Load schema:" +mysql -c -h 127.0.0.1 -P 3306 -u root -p -e "$(cat schema.sql)" From 03981f7ce5377edae368ad23ae33a1b989445fc5 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 6 Feb 2026 19:47:34 -0600 Subject: [PATCH 020/204] updated schema --- app/server/database/schema.sql | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/app/server/database/schema.sql b/app/server/database/schema.sql index f72bc55..2c8b9da 100644 --- a/app/server/database/schema.sql +++ b/app/server/database/schema.sql @@ -103,19 +103,33 @@ CREATE TABLE finus.asset ( ); CREATE TABLE finus.investment ( - account_id INTEGER NOT NULL, - asset_id INTEGER NOT NULL, - name VARCHAR(50) NOT NULL, - quantity INTEGER NOT NULL DEFAULT 1, - cost INTEGER NOT NULL, - last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (account_id, asset_id), - FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE, - FOREIGN KEY (asset_id) REFERENCES finus.asset(id) ON DELETE CASCADE + id INTEGER NOT NULL AUTO_INCREMENT, + account_id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + description VARCHAR(500) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE ); +CREATE TABLE finus.investmentInterestRate ( + investment_id INTEGER NOT NULL, + rate DOUBLE NOT NULL, + frequency_years INTEGER NOT NULL, + frequency_months INTEGER NOT NULL, + PRIMARY KEY (investment_id), + FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE +); + +CREATE TABLE finus.investmentState ( + investment_id INTEGER NOT NULL, + quantity DOUBLE NOT NULL DEFAULT 1, + total_cost DOUBLE NOT NULL, + at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (investment_id), + FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE +); #Populate lookup tables #INSERT INTO finus.financialAccountType (type) VALUES (); #INSERT INTO finus.financialAccountSubtype (type) VALUES (); -INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); + INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); From 4e726c3222cc90153563585fdd054068e008da45 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 6 Feb 2026 20:12:50 -0600 Subject: [PATCH 021/204] hopefuly final change to special investment type strategy --- app/server/database/schema.sql | 38 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/app/server/database/schema.sql b/app/server/database/schema.sql index 2c8b9da..d4d58d1 100644 --- a/app/server/database/schema.sql +++ b/app/server/database/schema.sql @@ -102,23 +102,22 @@ CREATE TABLE finus.asset ( PRIMARY KEY (id) ); +CREATE TABLE finus.investmentType( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + CREATE TABLE finus.investment ( id INTEGER NOT NULL AUTO_INCREMENT, account_id INTEGER NOT NULL, + type VARCHAR(50) NOT NULL, name VARCHAR(50) NOT NULL, description VARCHAR(500) NOT NULL, PRIMARY KEY (id), - FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE + FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE, + FOREIGN KEY (type) REFERENCES finus.investmentType(type) ); -CREATE TABLE finus.investmentInterestRate ( - investment_id INTEGER NOT NULL, - rate DOUBLE NOT NULL, - frequency_years INTEGER NOT NULL, - frequency_months INTEGER NOT NULL, - PRIMARY KEY (investment_id), - FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE -); CREATE TABLE finus.investmentState ( investment_id INTEGER NOT NULL, @@ -129,7 +128,26 @@ CREATE TABLE finus.investmentState ( FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE ); + +## Example of adding extra data +CREATE TABLE finus.stockInvestment( + investment_id INTEGER NOT NULL, + symbol VARCHAR(50) NOT NULL, + PRIMARY KEY (investment_id), + FOREIGN KEY (investment_id) REFERENCES finus.investment(id) +); + +CREATE TABLE finus.fixedInterestInvestment( + investment_id INTEGER NOT NULL, + rate DOUBLE NOT NULL, + frequency_years INTEGER NOT NULL, + frequency_months INTEGER NOT NULL, + PRIMARY KEY (investment_id), + FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE +); + #Populate lookup tables #INSERT INTO finus.financialAccountType (type) VALUES (); #INSERT INTO finus.financialAccountSubtype (type) VALUES (); - INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); +INSERT INTO finus.investmentType (type) VALUES ('fixedInterest'), ('stock'); #These have to match table names +INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); From 63b670e417479a58db63a8cf9531db23c892ae70 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Fri, 6 Feb 2026 22:22:30 -0600 Subject: [PATCH 022/204] Client: Created header for dashboard --- app/client/package-lock.json | 5016 +++++++++++++++++ app/client/package.json | 1 + app/client/src/App.css | 5 +- app/client/src/components/AccountCard.tsx | 9 + app/client/src/components/Header.tsx | 16 +- app/client/src/components/Layout.tsx | 3 +- app/client/src/components/NavBar.tsx | 9 + .../src/components/TransactionTable.tsx | 9 + app/client/src/index.css | 3 - app/client/src/pages/DashboardPage.tsx | 4 +- app/client/src/types/Transaction.tsx | 7 + 11 files changed, 5070 insertions(+), 12 deletions(-) create mode 100644 app/client/package-lock.json create mode 100644 app/client/src/components/AccountCard.tsx create mode 100644 app/client/src/components/NavBar.tsx create mode 100644 app/client/src/components/TransactionTable.tsx diff --git a/app/client/package-lock.json b/app/client/package-lock.json new file mode 100644 index 0000000..2fee72d --- /dev/null +++ b/app/client/package-lock.json @@ -0,0 +1,5016 @@ +{ + "name": "finus-client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "finus-client", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "@types/react-router-dom": "^5.3.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "radix-ui": "^1.4.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.13.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^25.2.0", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "npm:rolldown-vite@7.2.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.97.0", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.97.0", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz", + "integrity": "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.50.tgz", + "integrity": "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.50", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.50.tgz", + "integrity": "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.50.tgz", + "integrity": "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.50.tgz", + "integrity": "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.50.tgz", + "integrity": "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.50.tgz", + "integrity": "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.50.tgz", + "integrity": "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.50.tgz", + "integrity": "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.50.tgz", + "integrity": "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.50.tgz", + "integrity": "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.50.tgz", + "integrity": "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.50.tgz", + "integrity": "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.15.11", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.11", + "@swc/core-darwin-x64": "1.15.11", + "@swc/core-linux-arm-gnueabihf": "1.15.11", + "@swc/core-linux-arm64-gnu": "1.15.11", + "@swc/core-linux-arm64-musl": "1.15.11", + "@swc/core-linux-x64-gnu": "1.15.11", + "@swc/core-linux-x64-musl": "1.15.11", + "@swc/core-win32-arm64-msvc": "1.15.11", + "@swc/core-win32-ia32-msvc": "1.15.11", + "@swc/core-win32-x64-msvc": "1.15.11" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz", + "integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz", + "integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz", + "integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz", + "integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz", + "integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz", + "integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz", + "integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz", + "integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz", + "integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.30.2", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss/node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/history": { + "version": "4.7.11", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2", + "@swc/core": "^1.15.11" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "19.2.4", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.50", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.97.0", + "@rolldown/pluginutils": "1.0.0-beta.50" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.50", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", + "@rolldown/binding-darwin-x64": "1.0.0-beta.50", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.50", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "name": "rolldown-vite", + "version": "7.2.5", + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.97.0", + "fdir": "^6.5.0", + "lightningcss": "^1.30.2", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-beta.50", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.25.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/app/client/package.json b/app/client/package.json index 180a201..128cd97 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -18,6 +18,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.13.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" diff --git a/app/client/src/App.css b/app/client/src/App.css index b9d355d..baad9e5 100644 --- a/app/client/src/App.css +++ b/app/client/src/App.css @@ -1,8 +1,5 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + } .logo { diff --git a/app/client/src/components/AccountCard.tsx b/app/client/src/components/AccountCard.tsx new file mode 100644 index 0000000..a49d5d8 --- /dev/null +++ b/app/client/src/components/AccountCard.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function AccountCard() { + return ( +
AccountCard
+ ) +} + +export default AccountCard \ No newline at end of file diff --git a/app/client/src/components/Header.tsx b/app/client/src/components/Header.tsx index 79922da..69f5a06 100644 --- a/app/client/src/components/Header.tsx +++ b/app/client/src/components/Header.tsx @@ -1,8 +1,20 @@ import React from 'react' - +import { VscThreeBars } from "react-icons/vsc"; +import { IoPersonCircleSharp } from "react-icons/io5"; function Header() { return ( -
Header
+
+ +
+

Dashboard

+
+ {/*
*/} + +
) } diff --git a/app/client/src/components/Layout.tsx b/app/client/src/components/Layout.tsx index 235d5d0..54507f8 100644 --- a/app/client/src/components/Layout.tsx +++ b/app/client/src/components/Layout.tsx @@ -1,11 +1,12 @@ import React from 'react' import { Outlet, useLocation } from 'react-router-dom' +import Header from './Header' function Layout() { const location = useLocation() return ( <> -
From Layout - current path: {location.pathname}
+
) diff --git a/app/client/src/components/NavBar.tsx b/app/client/src/components/NavBar.tsx new file mode 100644 index 0000000..f7a8dba --- /dev/null +++ b/app/client/src/components/NavBar.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function NavBar() { + return ( +
NavBar
+ ) +} + +export default NavBar \ No newline at end of file diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx new file mode 100644 index 0000000..fb52a3e --- /dev/null +++ b/app/client/src/components/TransactionTable.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function TransactionTable() { + return ( +
TransactionTable
+ ) +} + +export default TransactionTable \ No newline at end of file diff --git a/app/client/src/index.css b/app/client/src/index.css index be6ae17..4f29e1d 100644 --- a/app/client/src/index.css +++ b/app/client/src/index.css @@ -59,9 +59,6 @@ a:hover { } body { - margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 667fc1f..9ea7886 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -1,8 +1,8 @@ import React from 'react' - +import type { Transaction } from '@/types/Transaction' function DashboardPage() { return ( -
From DashboardPage
+
From DashboardPage
) } diff --git a/app/client/src/types/Transaction.tsx b/app/client/src/types/Transaction.tsx index e69de29..ef19ba5 100644 --- a/app/client/src/types/Transaction.tsx +++ b/app/client/src/types/Transaction.tsx @@ -0,0 +1,7 @@ +export type Transaction = { + id: string; + amount: number; + category: string; + date: string; // ISO format date string + description?: string; +}; \ No newline at end of file From 41b981b65b18161e9393ffc2ddb3231a5242d2da Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Sun, 8 Feb 2026 17:40:51 -0600 Subject: [PATCH 023/204] Defined models for profiles, accounts, and transactions. --- .../services/database/models/FinancialAccount.ts | 10 ++++++++++ app/server/services/database/models/Profile.ts | 6 ++++++ app/server/services/database/models/ProfileLink.ts | 5 +++++ app/server/services/database/models/Transaction.ts | 10 ++++++++++ 4 files changed, 31 insertions(+) create mode 100644 app/server/services/database/models/FinancialAccount.ts create mode 100644 app/server/services/database/models/Profile.ts create mode 100644 app/server/services/database/models/ProfileLink.ts create mode 100644 app/server/services/database/models/Transaction.ts diff --git a/app/server/services/database/models/FinancialAccount.ts b/app/server/services/database/models/FinancialAccount.ts new file mode 100644 index 0000000..babf53c --- /dev/null +++ b/app/server/services/database/models/FinancialAccount.ts @@ -0,0 +1,10 @@ +// shows account types (chequing, savings, or investment accounts) +export interface FinancialAccount { + id: number; + name: string; + type: string; + balance: number; + value: number; + subtype?: string; + last_updated: Date; +} diff --git a/app/server/services/database/models/Profile.ts b/app/server/services/database/models/Profile.ts new file mode 100644 index 0000000..8b60711 --- /dev/null +++ b/app/server/services/database/models/Profile.ts @@ -0,0 +1,6 @@ +// users view of accounts and goals +export interface Profile { + id: number; + name: string; + description?: string; +} diff --git a/app/server/services/database/models/ProfileLink.ts b/app/server/services/database/models/ProfileLink.ts new file mode 100644 index 0000000..9a4edb0 --- /dev/null +++ b/app/server/services/database/models/ProfileLink.ts @@ -0,0 +1,5 @@ +//shows relationship betwen user and their profiles/accounts +export interface UserProfileLink { + profile_id: number; + account_id: number; +} diff --git a/app/server/services/database/models/Transaction.ts b/app/server/services/database/models/Transaction.ts new file mode 100644 index 0000000..bc6e5be --- /dev/null +++ b/app/server/services/database/models/Transaction.ts @@ -0,0 +1,10 @@ +//represents individual transaction events (spending, income, transfers) +export interface Transaction { + id: number; + financialAccount_id: number; + amount: number; + description?: string; + sender?: string; + recipient?: string; + date: Date; +} From a1d06f0ef899e02e2e5a0d9128329808d38211a4 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Sun, 8 Feb 2026 17:48:10 -0600 Subject: [PATCH 024/204] Moved model files into a src folder --- app/server/services/database/{ => src}/models/FinancialAccount.ts | 0 app/server/services/database/{ => src}/models/Profile.ts | 0 app/server/services/database/{ => src}/models/ProfileLink.ts | 0 app/server/services/database/{ => src}/models/Transaction.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename app/server/services/database/{ => src}/models/FinancialAccount.ts (100%) rename app/server/services/database/{ => src}/models/Profile.ts (100%) rename app/server/services/database/{ => src}/models/ProfileLink.ts (100%) rename app/server/services/database/{ => src}/models/Transaction.ts (100%) diff --git a/app/server/services/database/models/FinancialAccount.ts b/app/server/services/database/src/models/FinancialAccount.ts similarity index 100% rename from app/server/services/database/models/FinancialAccount.ts rename to app/server/services/database/src/models/FinancialAccount.ts diff --git a/app/server/services/database/models/Profile.ts b/app/server/services/database/src/models/Profile.ts similarity index 100% rename from app/server/services/database/models/Profile.ts rename to app/server/services/database/src/models/Profile.ts diff --git a/app/server/services/database/models/ProfileLink.ts b/app/server/services/database/src/models/ProfileLink.ts similarity index 100% rename from app/server/services/database/models/ProfileLink.ts rename to app/server/services/database/src/models/ProfileLink.ts diff --git a/app/server/services/database/models/Transaction.ts b/app/server/services/database/src/models/Transaction.ts similarity index 100% rename from app/server/services/database/models/Transaction.ts rename to app/server/services/database/src/models/Transaction.ts From 186e17d2fa9a21aabc1eb62d97e72c3832946907 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Sun, 8 Feb 2026 19:01:36 -0600 Subject: [PATCH 025/204] created Api routes for the db (accounts, transactions, profiles and index) --- .../services/database/src/db/connection.ts | 2 + .../services/database/src/routes/account.ts | 26 +++++++++++ .../services/database/src/routes/index.ts | 13 ++++++ .../services/database/src/routes/profile.ts | 27 +++++++++++ .../database/src/routes/transaction.ts | 46 +++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 app/server/services/database/src/db/connection.ts create mode 100644 app/server/services/database/src/routes/account.ts create mode 100644 app/server/services/database/src/routes/index.ts create mode 100644 app/server/services/database/src/routes/profile.ts create mode 100644 app/server/services/database/src/routes/transaction.ts diff --git a/app/server/services/database/src/db/connection.ts b/app/server/services/database/src/db/connection.ts new file mode 100644 index 0000000..7ebc42f --- /dev/null +++ b/app/server/services/database/src/db/connection.ts @@ -0,0 +1,2 @@ +// TODO: create connection and export it + diff --git a/app/server/services/database/src/routes/account.ts b/app/server/services/database/src/routes/account.ts new file mode 100644 index 0000000..77caaa2 --- /dev/null +++ b/app/server/services/database/src/routes/account.ts @@ -0,0 +1,26 @@ +// account creation route to db service + +//TODO import db connection +import { Router, Request, Response } from "express"; +import { db } from "../db/connection"; + +export const accountsRouter = Router(); + +accountsRouter.post("/", async (req: Request, res: Response) => { + try { + const { name, type, balance, value, subtype } = req.body; + + const [result] = await db.query( + `INSERT INTO financialAccount (name, type, balance, value, subtype) + VALUES (?, ?, ?, ?, ?)`, + [name, type, balance, value, subtype ?? null] + ); + + res.json({ + message: "Account successfully created", + id: result.insertId + }); + } catch (err) { + console.error("Account creation failed", err); + } +}); diff --git a/app/server/services/database/src/routes/index.ts b/app/server/services/database/src/routes/index.ts new file mode 100644 index 0000000..915ce6b --- /dev/null +++ b/app/server/services/database/src/routes/index.ts @@ -0,0 +1,13 @@ +//Index file for database service, sets up express server and routes + +import express from "express"; +import { accountsRouter } from "./account"; +import { transactionsRouter } from "./transaction"; +import { profilesRouter } from "./profile"; + +const app = express(); +app.use(express.json()); + +app.use("/accounts", accountsRouter); +app.use("/transactions", transactionsRouter); +app.use("/profiles", profilesRouter); \ No newline at end of file diff --git a/app/server/services/database/src/routes/profile.ts b/app/server/services/database/src/routes/profile.ts new file mode 100644 index 0000000..6c4cce4 --- /dev/null +++ b/app/server/services/database/src/routes/profile.ts @@ -0,0 +1,27 @@ +// profile creation route this lets us create profiles in the db , which can be used to group accounts together and track them. + + +import { Router, Request, Response } from "express"; +import { db } from "../db/connection"; + +export const profilesRouter = Router(); + +// Create a profile +profilesRouter.post("/", async (req: Request, res: Response) => { + try { + const { name, description } = req.body; + + const [result] = await db.query( + `INSERT INTO profile (name, description) + VALUES (?, ?)`, + [name, description ?? null] + ); + + res.json({ + message: "Profile successfully created", + id: result.insertId + }); + } catch (err) { + console.error("Profile creation failed:", err); + } +}); diff --git a/app/server/services/database/src/routes/transaction.ts b/app/server/services/database/src/routes/transaction.ts new file mode 100644 index 0000000..f876532 --- /dev/null +++ b/app/server/services/database/src/routes/transaction.ts @@ -0,0 +1,46 @@ +// transaction routes for creating and updating transactions +import { Router, Request, Response } from "express"; +import { db } from "../db/connection"; + +export const transactionsRouter = Router(); + +//creating a new transaction + +transactionsRouter.post("/", async (req: Request, res: Response) => { + try { + const { financialAccount_id, amount, description, sender, recipient, date } = req.body; + + const [result] = await db.query( + `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) + VALUES (?, ?, ?, ?, ?, ?)`, + [financialAccount_id, amount, description ?? null, sender ?? null, recipient ?? null, date] + ); + + res.json({ + message: "Transaction successfully created", + id: result.insertId + }); + } catch (err) { + console.error("Transaction creation failed", err); + } +}); + + + +// Update an existing transaction +transactionsRouter.put("/:id", async (req: Request, res: Response) => { + try { + const id = req.params.id; + const {amount,description, sender, recipient, date} = req.body; + + await db.query( + `UPDATE transaction + SET amount=?, description=?, sender=?, recipient=?, date=? + WHERE id=?`, + [amount, description ?? null, sender ?? null, recipient ?? null, date, id] + ); + + res.json({ message: "Transaction successfully updated" }); + } catch (err) { + console.error("Transaction update failed", err); } +}); From f374a460fb35df109f0393d3bc23bc6155e65667 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Mon, 9 Feb 2026 05:15:16 -0600 Subject: [PATCH 026/204] Created connection.ts. Altered the models to extend "RowDataPacket" --- app/server/services/database/src/db/connection.ts | 12 ++++++++++++ .../services/database/src/models/FinancialAccount.ts | 3 ++- app/server/services/database/src/models/Profile.ts | 3 ++- .../services/database/src/models/ProfileLink.ts | 3 ++- .../services/database/src/models/Transaction.ts | 3 ++- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/server/services/database/src/db/connection.ts b/app/server/services/database/src/db/connection.ts index 7ebc42f..9699aa7 100644 --- a/app/server/services/database/src/db/connection.ts +++ b/app/server/services/database/src/db/connection.ts @@ -1,2 +1,14 @@ // TODO: create connection and export it +import mysql from "mysql2/promise" + +export const db = mysql.createPool({ + + //hard coded values + host: 'database', + port: 3306, + user: 'root', + password: 'a', + database: "Finus" + +}); diff --git a/app/server/services/database/src/models/FinancialAccount.ts b/app/server/services/database/src/models/FinancialAccount.ts index babf53c..5f9f862 100644 --- a/app/server/services/database/src/models/FinancialAccount.ts +++ b/app/server/services/database/src/models/FinancialAccount.ts @@ -1,5 +1,6 @@ +import { RowDataPacket } from "mysql2"; // shows account types (chequing, savings, or investment accounts) -export interface FinancialAccount { +export interface FinancialAccount extends RowDataPacket { id: number; name: string; type: string; diff --git a/app/server/services/database/src/models/Profile.ts b/app/server/services/database/src/models/Profile.ts index 8b60711..77fb30a 100644 --- a/app/server/services/database/src/models/Profile.ts +++ b/app/server/services/database/src/models/Profile.ts @@ -1,5 +1,6 @@ +import { RowDataPacket } from "mysql2"; // users view of accounts and goals -export interface Profile { +export interface Profile extends RowDataPacket { id: number; name: string; description?: string; diff --git a/app/server/services/database/src/models/ProfileLink.ts b/app/server/services/database/src/models/ProfileLink.ts index 9a4edb0..0a792d6 100644 --- a/app/server/services/database/src/models/ProfileLink.ts +++ b/app/server/services/database/src/models/ProfileLink.ts @@ -1,5 +1,6 @@ +import { RowDataPacket } from "mysql2"; //shows relationship betwen user and their profiles/accounts -export interface UserProfileLink { +export interface UserProfileLink extends RowDataPacket{ profile_id: number; account_id: number; } diff --git a/app/server/services/database/src/models/Transaction.ts b/app/server/services/database/src/models/Transaction.ts index bc6e5be..126c611 100644 --- a/app/server/services/database/src/models/Transaction.ts +++ b/app/server/services/database/src/models/Transaction.ts @@ -1,5 +1,6 @@ +import { RowDataPacket } from "mysql2"; //represents individual transaction events (spending, income, transfers) -export interface Transaction { +export interface Transaction extends RowDataPacket{ id: number; financialAccount_id: number; amount: number; From 468dd408604346485fe353fc6a8762b3e5f49228 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Mon, 9 Feb 2026 09:50:45 -0600 Subject: [PATCH 027/204] Client: Introduced the first chart --- app/client/package-lock.json | 30 +++++++++++++ app/client/package.json | 2 + app/client/src/App.css | 2 +- app/client/src/App.tsx | 8 ++-- app/client/src/components/Header.tsx | 30 +++++++------ app/client/src/components/NavBar.tsx | 13 +++++- app/client/src/pages/DashboardPage.tsx | 61 +++++++++++++++++++++++++- 7 files changed, 125 insertions(+), 21 deletions(-) diff --git a/app/client/package-lock.json b/app/client/package-lock.json index 2fee72d..65620ec 100644 --- a/app/client/package-lock.json +++ b/app/client/package-lock.json @@ -10,11 +10,13 @@ "dependencies": { "@tailwindcss/vite": "^4.1.18", "@types/react-router-dom": "^5.3.3", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", "radix-ui": "^1.4.3", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", "react-icons": "^5.5.0", "react-router-dom": "^7.13.0", @@ -520,6 +522,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -3346,6 +3354,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "license": "Apache-2.0", @@ -4466,6 +4486,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.4", "license": "MIT", diff --git a/app/client/package.json b/app/client/package.json index 128cd97..44980a1 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -12,11 +12,13 @@ "dependencies": { "@tailwindcss/vite": "^4.1.18", "@types/react-router-dom": "^5.3.3", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", "radix-ui": "^1.4.3", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", "react-icons": "^5.5.0", "react-router-dom": "^7.13.0", diff --git a/app/client/src/App.css b/app/client/src/App.css index baad9e5..9057485 100644 --- a/app/client/src/App.css +++ b/app/client/src/App.css @@ -1,5 +1,5 @@ #root { - + min-height: calc(100vh); } .logo { diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 6ba05b8..f387871 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -22,17 +22,17 @@ function App() { function Main() { const location = useLocation() - + const main = true return ( }> {/* } /> } /> */} {/* Add a 404 page */} - }> + {main ? }> } /> - - {/*} />*/} + : + } />} ) diff --git a/app/client/src/components/Header.tsx b/app/client/src/components/Header.tsx index 69f5a06..66201ad 100644 --- a/app/client/src/components/Header.tsx +++ b/app/client/src/components/Header.tsx @@ -1,20 +1,24 @@ -import React from 'react' +import {useState} from 'react' import { VscThreeBars } from "react-icons/vsc"; import { IoPersonCircleSharp } from "react-icons/io5"; +import NavBar from './NavBar'; function Header() { + const [isSidebarOpen, setIsSidebarOpen] = useState(false) + const toggleSidebar = () => { + setIsSidebarOpen(prev => !prev) + } + return ( -
- -
-

Dashboard

-
- {/**/} - -
+ <> + {isSidebarOpen && } +
+ +
+

Dashboard

+
+ +
+ ) } diff --git a/app/client/src/components/NavBar.tsx b/app/client/src/components/NavBar.tsx index f7a8dba..7f6102c 100644 --- a/app/client/src/components/NavBar.tsx +++ b/app/client/src/components/NavBar.tsx @@ -1,8 +1,17 @@ import React from 'react' - +import '../styles/navbar.css' function NavBar() { + /* */ return ( -
NavBar
+
+ +
) } diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 9ea7886..8ac0e04 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -1,8 +1,67 @@ import React from 'react' import type { Transaction } from '@/types/Transaction' +import { Chart, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; +import { Pie } from 'react-chartjs-2'; +Chart.register(ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); function DashboardPage() { + const data = { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 20, + circumference: 360, + }] + }; + const options: any = { + responsive: true, + plugins: { + legend: { + position: 'right', + }, + title: { + display: true, + text: 'Expenses Breakdown', + font: { + size: 24, + weight: 'bold' + } + }, + layout: { + padding: 500, + + }, + legendDistance: { + padding: 50 + } + + }, + }; + + return ( -
From DashboardPage
+
+

Welcome Username

+

Here you can view your recent transactions and manage your finances.

+
+
+

Income vs Expenses

+
+
+ +
+
+

Recent Transactions

+
) } From 4b06c3cf16ddce4e14a93ba14e5af006fe912aac Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Mon, 9 Feb 2026 10:19:19 -0600 Subject: [PATCH 028/204] Client: Added cards on dashboard page --- app/client/src/components/NavBar.tsx | 1 - app/client/src/pages/DashboardPage.tsx | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/client/src/components/NavBar.tsx b/app/client/src/components/NavBar.tsx index 7f6102c..30107cb 100644 --- a/app/client/src/components/NavBar.tsx +++ b/app/client/src/components/NavBar.tsx @@ -1,5 +1,4 @@ import React from 'react' -import '../styles/navbar.css' function NavBar() { /*
{}}>× diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 8ac0e04..1767272 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -51,7 +51,29 @@ function DashboardPage() { return (

Welcome Username

-

Here you can view your recent transactions and manage your finances.

+

Here you can view your recent transactions and manage your finances.

+
+
+

Total Balance

+

$5,000

+
+
+

Current Income

+

$5,000

+
+
+

Average Expenses

+

$100,000

+
+
+

Current Debt

+

$50,000

+
+
+

Total Savings

+

$100,000,000

+
+

Income vs Expenses

From d351091dae4a169408bf6f2a582e447034ddd397 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Mon, 9 Feb 2026 10:46:07 -0600 Subject: [PATCH 029/204] Client: Refactored account card to a component --- app/client/src/components/AccountCard.tsx | 10 +++++---- app/client/src/pages/DashboardPage.tsx | 26 ++++++----------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/app/client/src/components/AccountCard.tsx b/app/client/src/components/AccountCard.tsx index a49d5d8..7a8c4ff 100644 --- a/app/client/src/components/AccountCard.tsx +++ b/app/client/src/components/AccountCard.tsx @@ -1,8 +1,10 @@ -import React from 'react' - -function AccountCard() { +function AccountCard(props: any) { + const { title, amount, backgroundColor } = props; return ( -
AccountCard
+
+

{title}

+

{amount}

+
) } diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 1767272..3683253 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -2,6 +2,7 @@ import React from 'react' import type { Transaction } from '@/types/Transaction' import { Chart, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; import { Pie } from 'react-chartjs-2'; +import AccountCard from '@/components/AccountCard'; Chart.register(ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); function DashboardPage() { const data = { @@ -53,26 +54,11 @@ function DashboardPage() {

Welcome Username

Here you can view your recent transactions and manage your finances.

-
-

Total Balance

-

$5,000

-
-
-

Current Income

-

$5,000

-
-
-

Average Expenses

-

$100,000

-
-
-

Current Debt

-

$50,000

-
-
-

Total Savings

-

$100,000,000

-
+ + + + +
From e6c9dea8c9d816049f1b466dd02912f5aef526f9 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Tue, 10 Feb 2026 12:09:14 -0600 Subject: [PATCH 030/204] Client: Added second chart on dashboard page --- app/client/src/components/AccountCard.tsx | 9 +++- app/client/src/pages/DashboardPage.tsx | 51 +++++++++++++++++++---- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/app/client/src/components/AccountCard.tsx b/app/client/src/components/AccountCard.tsx index 7a8c4ff..519ec3b 100644 --- a/app/client/src/components/AccountCard.tsx +++ b/app/client/src/components/AccountCard.tsx @@ -1,7 +1,12 @@ -function AccountCard(props: any) { +type AccountCardProps = { + title: string; + amount: string; + backgroundColor: string | "#2a2a2a"; +} +function AccountCard(props: AccountCardProps) { const { title, amount, backgroundColor } = props; return ( -
+

{title}

{amount}

diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 3683253..49849e0 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -1,9 +1,9 @@ import React from 'react' import type { Transaction } from '@/types/Transaction' -import { Chart, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; -import { Pie } from 'react-chartjs-2'; +import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; +import { Pie, Line } from 'react-chartjs-2'; import AccountCard from '@/components/AccountCard'; -Chart.register(ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); +Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); function DashboardPage() { const data = { labels: [ @@ -23,18 +23,18 @@ function DashboardPage() { circumference: 360, }] }; - const options: any = { + const options = { responsive: true, plugins: { legend: { - position: 'right', + position: 'right' as const, }, title: { display: true, text: 'Expenses Breakdown', font: { size: 24, - weight: 'bold' + weight: 'bold' as const } }, layout: { @@ -49,6 +49,43 @@ function DashboardPage() { }; + const linearOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Income vs Expenses', + font: { + size: 24, + weight: 'bold' as const + } + }, + }, + }; + + const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; + + const linearData = { + labels, + datasets: [ + { + label: 'Income', + data: ["2374823", "83932", "8294","102190"], + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.5)', + }, + { + label: 'Expenses', + data: ["2374", "9938", "122","8723"], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }, + ], + }; + return (

Welcome Username

@@ -62,7 +99,7 @@ function DashboardPage() {
-

Income vs Expenses

+
From e784495af55b1430cc653ce552751493153b3e4d Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Tue, 10 Feb 2026 16:04:53 -0600 Subject: [PATCH 031/204] Fixed user auth login and signup to work with microservices --- app/client/src/App.css | 198 +++++++-- app/client/src/App.tsx | 389 +++++++++++++++--- app/client/src/index.css | 71 +--- app/server/database/schema.sql | 12 +- app/server/docker-compose.yml | 16 +- app/server/services/api-gateway/src/index.ts | 55 ++- app/server/services/auth/package.json | 3 +- .../services/auth/src/credentialUtils.ts | 18 +- app/server/services/auth/src/db.ts | 112 +++++ app/server/services/auth/src/index.ts | 240 ++++++----- 10 files changed, 863 insertions(+), 251 deletions(-) create mode 100644 app/server/services/auth/src/db.ts diff --git a/app/client/src/App.css b/app/client/src/App.css index b9d355d..58d94b9 100644 --- a/app/client/src/App.css +++ b/app/client/src/App.css @@ -1,42 +1,188 @@ -#root { - max-width: 1280px; - margin: 0 auto; +.auth-shell { + position: relative; + min-height: 100vh; + overflow: hidden; +} + +.auth-glow { + position: absolute; + width: 28rem; + height: 28rem; + border-radius: 50%; + opacity: 0.25; + filter: blur(90px); + pointer-events: none; + animation: float 9s ease-in-out infinite; +} + +.auth-glow-left { + background: radial-gradient(circle, #18cc5f 0%, #0d4d26 70%, transparent 100%); + top: -8rem; + left: -8rem; +} + +.auth-glow-right { + background: radial-gradient(circle, #27a552 0%, #0f411d 65%, transparent 100%); + right: -8rem; + bottom: -8rem; + animation-delay: 1.2s; +} + +.auth-layout { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + position: relative; + z-index: 1; +} + +.auth-panel { + width: min(26rem, 100%); + background: rgba(9, 16, 11, 0.9); + border: 1px solid rgba(75, 255, 122, 0.25); + border-radius: 1.2rem; + box-shadow: 0 18px 45px rgba(0, 0, 0, 0.45); padding: 2rem; + backdrop-filter: blur(6px); + animation: rise-in 0.5s ease-out; +} + +.dashboard-panel { text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; +.auth-tag { + display: inline-block; + margin: 0; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.16em; + padding: 0.25rem 0.55rem; + border-radius: 999px; + color: #94ffb4; + border: 1px solid rgba(148, 255, 180, 0.45); + background: rgba(53, 177, 95, 0.12); } -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); + +.auth-panel h1 { + margin: 1rem 0 0.4rem; + color: #d7ffe5; + font-size: clamp(1.55rem, 2vw + 1rem, 2rem); +} + +.auth-copy { + margin: 0; + color: #9acaa9; + line-height: 1.45; +} + +.auth-copy strong { + color: #d7ffe5; +} + +.auth-form { + margin-top: 1.4rem; + display: grid; + gap: 0.75rem; +} + +.auth-form label { + color: #a8e0b9; + font-size: 0.86rem; } -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); + +.auth-form input { + width: 100%; + border: 1px solid rgba(82, 164, 106, 0.5); + border-radius: 0.65rem; + background: #06100a; + color: #deffe9; + padding: 0.68rem 0.8rem; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + font-size: 0.95rem; +} + +.auth-form input::placeholder { + color: #79a588; } -@keyframes logo-spin { +.auth-form input:focus { + border-color: #4af07f; + box-shadow: 0 0 0 3px rgba(74, 240, 127, 0.18); +} + +.auth-error { + margin: 0.2rem 0 0; + color: #ff7f8f; + font-size: 0.9rem; +} + +.auth-button { + width: 100%; + margin-top: 0.5rem; + border: 0; + border-radius: 0.7rem; + padding: 0.78rem 1rem; + font-weight: 700; + letter-spacing: 0.02em; + cursor: pointer; + color: #041108; + background: linear-gradient(135deg, #53ff8c, #17ac4d); + transition: transform 0.2s, box-shadow 0.2s; +} + +.auth-button:hover { + transform: translateY(-1px); + box-shadow: 0 8px 18px rgba(42, 174, 85, 0.35); +} + +.auth-button:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.auth-button:active { + transform: translateY(0); +} + +.auth-switch { + margin: 1rem 0 0; + color: #8eb79d; + font-size: 0.93rem; +} + +.auth-switch a { + color: #8dffaf; + text-decoration: none; + font-weight: 600; +} + +.auth-switch a:hover { + text-decoration: underline; +} + +@keyframes rise-in { from { - transform: rotate(0deg); + opacity: 0; + transform: translateY(14px); } to { - transform: rotate(360deg); + opacity: 1; + transform: translateY(0); } } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; +@keyframes float { + 0%, + 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-18px); } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; } diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 522aa2f..7f6cb74 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -1,75 +1,368 @@ -/* -This is to be used as a rerouter to various website pages. -*/ - -import { Suspense, useState, lazy} from 'react' +import { useState, type FormEvent } from 'react' +import { BrowserRouter as Router, Link, Navigate, Route, Routes } from 'react-router-dom' import './App.css' -import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom' -import LoadingSpinner from './components/ui/LoadingSpinner.tsx' -function App() { +type AuthUser = { + id: number + email: string + name: string +} + +type AuthSession = { + token: string + user: AuthUser +} + +type AuthApiResponse = { + ok: boolean + token?: string + user?: AuthUser + error?: string + message?: string +} + +const SESSION_STORAGE_KEY = 'finus-session' +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000' + +function isValidAuthUser(value: unknown): value is AuthUser { + if (!value || typeof value !== 'object') { + return false + } + + const maybeUser = value as AuthUser return ( - -
- + typeof maybeUser.id === 'number' && + typeof maybeUser.email === 'string' && + typeof maybeUser.name === 'string' ) } +function loadSession(): AuthSession | null { + const raw = localStorage.getItem(SESSION_STORAGE_KEY) + if (!raw) { + return null + } + + try { + const parsed = JSON.parse(raw) as AuthSession + if (!parsed.token || !isValidAuthUser(parsed.user)) { + return null + } + + return parsed + } catch { + return null + } +} + +function saveSession(session: AuthSession) { + localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session)) +} + +function clearSession() { + localStorage.removeItem(SESSION_STORAGE_KEY) +} + +async function requestAuth(path: string, payload: Record): Promise { + try { + const response = await fetch(`${API_BASE_URL}${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + + const data = (await response.json().catch(() => null)) as AuthApiResponse | null + if (!data) { + return { ok: false, error: 'Invalid response from auth server.' } + } + + if (!response.ok || !data.ok) { + return { + ok: false, + error: data.error ?? 'Authentication failed.', + } + } + + if (!data.token || !isValidAuthUser(data.user)) { + return { ok: false, error: 'Auth response is missing required fields.' } + } + + return data + } catch { + return { ok: false, error: 'Unable to connect to auth server.' } + } +} + +function App() { + const [session, setSession] = useState(() => loadSession()) + function handleAuthSuccess(nextSession: AuthSession) { + saveSession(nextSession) + setSession(nextSession) + } -function Main() { - const location = useLocation() + function handleLogout() { + clearSession() + setSession(null) + } return ( -
- {/* Conditionally render Navbar - adjust logic as needed */} - {location.pathname !== '/' && location.pathname !== '/login'} - - }> + +
+
+
+ - {/* } /> - } /> */} - {/* Add a 404 page */} - } /> + } + /> + + ) : ( + + ) + } + /> + + ) : ( + + ) + } + /> + + ) : ( + + ) + } + /> + } /> - -
+
+ ) } -//default page for 404 - this can test the APIs for now -function NotFoundPage() { - const [count, setCount] = useState(0) - const [response, setResponse] = useState('') +type LoginPageProps = { + onLogin: (session: AuthSession) => void +} + +function LoginPage({ onLogin }: LoginPageProps) { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [errorMessage, setErrorMessage] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + async function handleSubmit(event: FormEvent) { + event.preventDefault() + setErrorMessage('') + setIsSubmitting(true) + + const result = await requestAuth('/api/auth/login', { + email: email.trim(), + password, + }) + + setIsSubmitting(false) + + if (!result.ok || !result.user || !result.token) { + setErrorMessage(result.error ?? 'Login failed.') + return + } + + onLogin({ + token: result.token, + user: result.user, + }) + } return ( - <> -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

- -

Response from server: {response}

+ + +

+ New here? Create an account +

-

- Click on the Vite and React logos to learn more -

- +
) } +type SignUpPageProps = { + onSignup: (session: AuthSession) => void +} + +function SignUpPage({ onSignup }: SignUpPageProps) { + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [errorMessage, setErrorMessage] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + async function handleSubmit(event: FormEvent) { + event.preventDefault() + setErrorMessage('') + + const cleanedName = name.trim() + const normalizedEmail = email.trim().toLowerCase() + + if (!cleanedName) { + setErrorMessage('Name is required.') + return + } + + if (password !== confirmPassword) { + setErrorMessage('Passwords do not match.') + return + } + + setIsSubmitting(true) + const result = await requestAuth('/api/auth/signup', { + name: cleanedName, + email: normalizedEmail, + password, + }) + setIsSubmitting(false) + + if (!result.ok || !result.user || !result.token) { + setErrorMessage(result.error ?? 'Signup failed.') + return + } + + onSignup({ + token: result.token, + user: result.user, + }) + } + + return ( +
+
+

Finus

+

Create account

+

Set up your account in less than a minute.

+
+ + setName(event.target.value)} + placeholder="Alex Patel" + required + /> -// broadcast to the API gate endpoint - should receive responses from all services that are set up -async function testAPI() { - const response = await fetch('http://localhost:3000/health').then(res => res.text()) - return response + + setEmail(event.target.value)} + placeholder="you@example.com" + required + /> + + + setPassword(event.target.value)} + placeholder="At least 8 characters" + required + /> + + + setConfirmPassword(event.target.value)} + placeholder="Repeat password" + required + /> + + {errorMessage ?

{errorMessage}

: null} + + +
+ +

+ Already have an account? Log in +

+
+
+ ) +} + +type DashboardPageProps = { + session: AuthSession + onLogout: () => void +} + +function DashboardPage({ session, onLogout }: DashboardPageProps) { + return ( +
+
+

Finus

+

Signed in

+

+ Hello {session.user.name}. You are logged in as {session.user.email}. +

+ +
+
+ ) } export default App diff --git a/app/client/src/index.css b/app/client/src/index.css index 08a3ac9..be55f8c 100644 --- a/app/client/src/index.css +++ b/app/client/src/index.css @@ -1,68 +1,33 @@ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap'); + :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + font-family: 'Space Grotesk', 'Segoe UI', Tahoma, sans-serif; line-height: 1.5; font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - + color: #e3ffec; + background: #030805; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +* { + box-sizing: border-box; } -body { +html, +body, +#root { + width: 100%; + min-height: 100%; margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +body { + min-width: 320px; + background: + radial-gradient(circle at 20% 10%, rgba(32, 123, 55, 0.28), transparent 38%), + radial-gradient(circle at 80% 85%, rgba(18, 87, 40, 0.28), transparent 42%), + #030805; } diff --git a/app/server/database/schema.sql b/app/server/database/schema.sql index d4d58d1..61a7645 100644 --- a/app/server/database/schema.sql +++ b/app/server/database/schema.sql @@ -1,4 +1,14 @@ -CREATE DATABASE finus; +CREATE DATABASE IF NOT EXISTS finus; + +CREATE TABLE finus.auth_user ( + id INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_auth_user_email (email) +); CREATE TABLE finus.finusAccount ( id INTEGER NOT NULL AUTO_INCREMENT, diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 5b1aa57..cecde65 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -18,6 +18,14 @@ services: build: ./services/auth env_file: - path: ./default.env + environment: + - DB_HOST=database + - DB_PORT=3306 + - DB_NAME=finus + - DB_USER=finus_app + - DB_PASSWORD=password + depends_on: + - database user: build: ./services/user @@ -39,13 +47,15 @@ services: image: mysql:8.0-debian volumes: - db_data:/var/lib/mysql - - ./services/database:/docker-entrypoint-initdb.d + - ./database/schema.sql:/docker-entrypoint-initdb.d/schema.sql secrets: - db_root_password - db_password environment: - - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password - - MYSQL_PASSWORD_FILE=/run/secrets/db_password + - MYSQL_DATABASE=finus + - MYSQL_USER=finus_app + - MYSQL_ROOT_PASSWORD=a + - MYSQL_PASSWORD=password ports: - "3306:3306" diff --git a/app/server/services/api-gateway/src/index.ts b/app/server/services/api-gateway/src/index.ts index f5676c8..bdac34b 100644 --- a/app/server/services/api-gateway/src/index.ts +++ b/app/server/services/api-gateway/src/index.ts @@ -14,6 +14,7 @@ import cors from 'cors'; const app = express(); const PORT = process.env.PORT || 8000; +const AUTH_SERVICE_ADDR = process.env.AUTH_SERVICE_ADDR || 'http://localhost:8000'; app.use(cors({ origin: 'http://localhost:8080', // vite dev server so that the client can access this API @@ -23,6 +24,47 @@ app.use(cors({ })); app.use(express.json()); + +async function forwardAuthRequest(req: express.Request, res: express.Response, path: string) { + try { + const authResponse = await fetch(`${AUTH_SERVICE_ADDR}${path}`, { + method: req.method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(req.body ?? {}) + }); + + const responseText = await authResponse.text(); + const responseType = authResponse.headers.get('content-type') || ''; + res.status(authResponse.status); + + if (responseType.includes('application/json')) { + res.type('application/json'); + try { + res.send(JSON.parse(responseText)); + return; + } catch { + res.send(responseText); + return; + } + } + + res.send(responseText); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unable to reach auth service.'; + res.status(502).json({ ok: false, error: message }); + } +} + +app.post('/api/auth/signup', async (req: express.Request, res: express.Response) => { + await forwardAuthRequest(req, res, '/api/auth/signup'); +}); + +app.post('/api/auth/login', async (req: express.Request, res: express.Response) => { + await forwardAuthRequest(req, res, '/api/auth/login'); +}); + const server = app.listen(PORT, () => { console.log(`API Gateway running on port ${PORT}`); }); @@ -30,15 +72,20 @@ process.on("SIGTERM", () => server.close()); //test endpoint - app.get('/health', async (req: express.Request, res: express.Response) => { - const result: { [string]: string} = {}; + app.get('/health', async (_req: express.Request, res: express.Response) => { + const result: Record = {}; const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); console.log(services); for(const service of services) { - console.log(process.env[service]); + const serviceAddress = process.env[service]; + if (!serviceAddress) { + continue; + } + + console.log(serviceAddress); const serviceName = service.split('_')[0]; - result[serviceName] = await fetch(`${process.env[service]}/health`) + result[serviceName] = await fetch(`${serviceAddress}/health`) .then((res) => res.text()) .catch((err) => err.message); } diff --git a/app/server/services/auth/package.json b/app/server/services/auth/package.json index 95efb4d..e13f809 100644 --- a/app/server/services/auth/package.json +++ b/app/server/services/auth/package.json @@ -24,7 +24,8 @@ "dependencies": { "bcrypt": "^6.0.0", "express": "^5.2.1", - "jsonwebtoken": "^9.0.3" + "jsonwebtoken": "^9.0.3", + "mysql2": "^3.15.3" }, "scripts": { "dev": "bun --watch src/index.ts", diff --git a/app/server/services/auth/src/credentialUtils.ts b/app/server/services/auth/src/credentialUtils.ts index 2580cfa..ab9db16 100644 --- a/app/server/services/auth/src/credentialUtils.ts +++ b/app/server/services/auth/src/credentialUtils.ts @@ -1,17 +1,21 @@ +const MIN_PASSWORD_LENGTH = 8 + function validatePassword(password: string): boolean { - return true; + const hasLetter = /[a-zA-Z]/.test(password) + const hasDigit = /\d/.test(password) + return password.length >= MIN_PASSWORD_LENGTH && hasLetter && hasDigit } function validateEmail(email: string): boolean { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) } -async function hashPassword(password: string) { - // return await Bun.password.hash(password); +async function hashPassword(password: string): Promise { + return Bun.password.hash(password) } -async function verifyPassword(password: string, hash: string) { - // return await Bun.password.verify(password, hash); +async function verifyPassword(password: string, hash: string): Promise { + return Bun.password.verify(password, hash) } -export { validatePassword, validateEmail }; +export { hashPassword, validateEmail, validatePassword, verifyPassword } diff --git a/app/server/services/auth/src/db.ts b/app/server/services/auth/src/db.ts new file mode 100644 index 0000000..ec06c07 --- /dev/null +++ b/app/server/services/auth/src/db.ts @@ -0,0 +1,112 @@ +import { readFileSync } from 'node:fs' +import mysql, { type ResultSetHeader, type RowDataPacket } from 'mysql2/promise' + +type DbUser = { + id: number + name: string + email: string + password_hash: string +} + +function resolveDbPassword(): string { + const inlinePassword = process.env.DB_PASSWORD + if (inlinePassword) { + return inlinePassword + } + + const passwordFile = process.env.DB_PASSWORD_FILE + if (passwordFile) { + return readFileSync(passwordFile, 'utf-8').trim() + } + + return 'password' +} + +const pool = mysql.createPool({ + host: process.env.DB_HOST ?? 'database', + port: Number(process.env.DB_PORT ?? 3306), + user: process.env.DB_USER ?? 'finus_app', + password: resolveDbPassword(), + database: process.env.DB_NAME ?? 'finus', + waitForConnections: true, + connectionLimit: 10, +}) + +const INIT_RETRIES = 30 +const INIT_DELAY_MS = 1000 + +function wait(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +let initPromise: Promise | null = null + +async function initializeSchema() { + await pool.execute(` +CREATE TABLE IF NOT EXISTS auth_user ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) +`) +} + +async function ensureInitialized() { + if (!initPromise) { + initPromise = (async () => { + let lastError: unknown = null + + for (let attempt = 1; attempt <= INIT_RETRIES; attempt += 1) { + try { + await initializeSchema() + return + } catch (error) { + lastError = error + if (attempt < INIT_RETRIES) { + await wait(INIT_DELAY_MS) + } + } + } + + throw lastError + })() + } + + await initPromise +} + +async function findUserByEmail(email: string): Promise { + await ensureInitialized() + + const [rows] = await pool.execute<(DbUser & RowDataPacket)[]>( + 'SELECT id, name, email, password_hash FROM auth_user WHERE email = ? LIMIT 1', + [email] + ) + + return rows[0] ?? null +} + +async function createUser(name: string, email: string, passwordHash: string): Promise { + await ensureInitialized() + + const [result] = await pool.execute( + 'INSERT INTO auth_user (name, email, password_hash) VALUES (?, ?, ?)', + [name, email, passwordHash] + ) + + return { + id: result.insertId, + name, + email, + password_hash: passwordHash, + } +} + +async function closeDatabase() { + await pool.end() +} + +export type { DbUser } +export { closeDatabase, createUser, findUserByEmail } diff --git a/app/server/services/auth/src/index.ts b/app/server/services/auth/src/index.ts index 35bc14f..3e55aa8 100644 --- a/app/server/services/auth/src/index.ts +++ b/app/server/services/auth/src/index.ts @@ -1,119 +1,143 @@ -import { stmts } from "./db"; - -const PORT = process.env.PORT || 8000; - -function json(body: unknown, status = 200) { - return new Response(JSON.stringify(body), { - status, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "http://localhost:5173", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - }); +import express from 'express' +import jwt from 'jsonwebtoken' +import { closeDatabase, createUser, findUserByEmail, type DbUser } from './db' +import { hashPassword, validateEmail, validatePassword, verifyPassword } from './credentialUtils' + +type SignupBody = { + name?: string + email?: string + password?: string } - -function badRequest(message: string) { - return json({ ok: false, error: message }, 400); +type LoginBody = { + email?: string + password?: string } - -function getOptions() { - return { - status: 204, - headers: { - "Access-Control-Allow-Origin": "http://localhost.*", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - } +type AuthTokenClaims = { + sub: string + email: string + name: string } -// ---- server ---- -Bun.serve({ - port: PORT, - async fetch(req) { - const url = new URL(req.url); - - if (req.method === "OPTIONS") { - return new Response(null, getOptions()) - }; - - if(url.pathname === '/api/auth/token' && req.method === "POST") { - let body; - try { - body = await req.json(); - if(!body.email || !body.password) { throw new Error() } - } catch { - return badRequest("Body must be JSON containing username and password."); - } - - if(!validateEmail(body.email)) { - return - } - if(!validatePassword(body.password)) { - - } - - - - } - - // ---- SIGNUP ---- - if (req.method === "POST" && url.pathname === "/api/auth/signup") { - - const email = body.email.trim().toLowerCase(); - const password = body.password; - - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); - if (password.length < 8) return badRequest("Password must be at least 8 characters."); +const PORT = Number(process.env.PORT ?? 8000) +const JWT_SECRET = process.env.AUTH_JWT_SECRET ?? 'dev-only-secret' - const existing = stmts.findByEmail.get(email) as any; - if (existing) return json({ ok: false, error: "Email already exists." }, 409); +const app = express() +app.use(express.json()) - const passwordHash = await hashPassword(password); - stmts.createUser.run(email, passwordHash); - - return json({ ok: true, message: "Account created." }, 201); - } - - // ---- LOGIN ---- - if (req.method === "POST" && url.pathname === "/api/auth/login") { - const body = (await req.json().catch(() => null)) as null | { - email?: string; - password?: string; - }; - - if (!body?.email || !body?.password) { - return badRequest("Email and password are required."); - } - - const email = body.email.trim().toLowerCase(); - const password = body.password; - - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); - - const user = stmts.findByEmail.get(email) as any; - if (!user) return json({ ok: false, error: "Invalid email or password." }, 401); - - const valid = await verifyPassword(password, user.password_hash); - if (!valid) return json({ ok: false, error: "Invalid email or password." }, 401); - - return json({ - ok: true, - message: "Logged in!", - user: { id: user.id, email: user.email }, - }); - } +function authResponseUser(user: DbUser) { + return { + id: user.id, + name: user.name, + email: user.email, + } +} - if (req.method === "GET" && url.pathname === "/api/health") { - return json({ ok: true, status: "up" }); - } +function issueToken(user: DbUser): string { + const payload: AuthTokenClaims = { + sub: String(user.id), + email: user.email, + name: user.name, + } - return json({ ok: false, error: "Not found" }, 404); - }, -}); + return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }) +} -console.log(`Auth service running on ${PORT}`); +app.post('/api/auth/signup', async (req, res) => { + const body = req.body as SignupBody + const name = body.name?.trim() + const email = body.email?.trim().toLowerCase() + const password = body.password ?? '' + + if (!name || !email || !password) { + res.status(400).json({ ok: false, error: 'Name, email and password are required.' }) + return + } + + if (!validateEmail(email)) { + res.status(400).json({ ok: false, error: 'Please enter a valid email address.' }) + return + } + + if (!validatePassword(password)) { + res.status(400).json({ + ok: false, + error: 'Password must be at least 8 characters and include letters and numbers.', + }) + return + } + + const existingUser = await findUserByEmail(email) + if (existingUser) { + res.status(409).json({ ok: false, error: 'An account with this email already exists.' }) + return + } + + const passwordHash = await hashPassword(password) + const user = await createUser(name, email, passwordHash) + const token = issueToken(user) + + res.status(201).json({ + ok: true, + message: 'Account created.', + token, + user: authResponseUser(user), + }) +}) + +app.post('/api/auth/login', async (req, res) => { + const body = req.body as LoginBody + const email = body.email?.trim().toLowerCase() + const password = body.password ?? '' + + if (!email || !password) { + res.status(400).json({ ok: false, error: 'Email and password are required.' }) + return + } + + if (!validateEmail(email)) { + res.status(400).json({ ok: false, error: 'Please enter a valid email address.' }) + return + } + + const user = await findUserByEmail(email) + if (!user) { + res.status(401).json({ ok: false, error: 'Invalid email or password.' }) + return + } + + const isValidPassword = await verifyPassword(password, user.password_hash) + if (!isValidPassword) { + res.status(401).json({ ok: false, error: 'Invalid email or password.' }) + return + } + + const token = issueToken(user) + + res.status(200).json({ + ok: true, + message: 'Logged in.', + token, + user: authResponseUser(user), + }) +}) + +app.get('/health', (_req, res) => { + res.send('ok') +}) + +app.get('/api/health', (_req, res) => { + res.json({ ok: true, status: 'up' }) +}) + +const server = app.listen(PORT, () => { + console.log(`Auth Service running on port ${PORT}`) +}) + +process.on('SIGTERM', () => { + server.close() + closeDatabase().catch((error: unknown) => { + console.error('Failed to close DB pool:', error) + }) +}) From fa8f3b201a2606db5071e0369ea70b5a0e2de9da Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Tue, 10 Feb 2026 16:38:34 -0600 Subject: [PATCH 032/204] Updated frontend to work with multiple tables in db --- app/client/src/App.tsx | 46 ++++++-- app/server/database/schema.sql | 16 +-- app/server/services/auth/src/db.ts | 140 ++++++++++++++++++++---- app/server/services/auth/src/index.ts | 17 ++- app/server/services/database/schema.sql | 18 +-- 5 files changed, 180 insertions(+), 57 deletions(-) diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 7f6cb74..c36ea87 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -237,7 +237,9 @@ type SignUpPageProps = { } function SignUpPage({ onSignup }: SignUpPageProps) { - const [name, setName] = useState('') + const [username, setUsername] = useState('') + const [firstName, setFirstName] = useState('') + const [lastName, setLastName] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') @@ -248,11 +250,13 @@ function SignUpPage({ onSignup }: SignUpPageProps) { event.preventDefault() setErrorMessage('') - const cleanedName = name.trim() + const cleanedUsername = username.trim() + const cleanedFirstName = firstName.trim() + const cleanedLastName = lastName.trim() const normalizedEmail = email.trim().toLowerCase() - if (!cleanedName) { - setErrorMessage('Name is required.') + if (!cleanedUsername || !cleanedFirstName || !cleanedLastName) { + setErrorMessage('Username, first name, and last name are required.') return } @@ -263,7 +267,9 @@ function SignUpPage({ onSignup }: SignUpPageProps) { setIsSubmitting(true) const result = await requestAuth('/api/auth/signup', { - name: cleanedName, + username: cleanedUsername, + firstName: cleanedFirstName, + lastName: cleanedLastName, email: normalizedEmail, password, }) @@ -288,13 +294,33 @@ function SignUpPage({ onSignup }: SignUpPageProps) {

Set up your account in less than a minute.

- + setName(event.target.value)} - placeholder="Alex Patel" + value={username} + onChange={(event) => setUsername(event.target.value)} + placeholder="alexp" + required + /> + + + setFirstName(event.target.value)} + placeholder="Alex" + required + /> + + + setLastName(event.target.value)} + placeholder="Patel" required /> diff --git a/app/server/database/schema.sql b/app/server/database/schema.sql index 61a7645..aa6a224 100644 --- a/app/server/database/schema.sql +++ b/app/server/database/schema.sql @@ -1,28 +1,18 @@ CREATE DATABASE IF NOT EXISTS finus; -CREATE TABLE finus.auth_user ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(100) NOT NULL, - email VARCHAR(100) NOT NULL, - password_hash VARCHAR(255) NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY uq_auth_user_email (email) -); - CREATE TABLE finus.finusAccount ( id INTEGER NOT NULL AUTO_INCREMENT, username VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, - age INTEGER NOT NULL, created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id) + PRIMARY KEY (id), + UNIQUE KEY uq_finus_account_email (email) ); CREATE TABLE finus.credentials ( - finus_account_id INTEGER NOT NULL AUTO_INCREMENT, + finus_account_id INTEGER NOT NULL, pw_hash BLOB(256) NOT NULL, salt CHAR(8) NOT NULL, PRIMARY KEY (finus_account_id), diff --git a/app/server/services/auth/src/db.ts b/app/server/services/auth/src/db.ts index ec06c07..2fd3119 100644 --- a/app/server/services/auth/src/db.ts +++ b/app/server/services/auth/src/db.ts @@ -1,5 +1,9 @@ import { readFileSync } from 'node:fs' -import mysql, { type ResultSetHeader, type RowDataPacket } from 'mysql2/promise' +import mysql, { + type PoolConnection, + type ResultSetHeader, + type RowDataPacket, +} from 'mysql2/promise' type DbUser = { id: number @@ -8,6 +12,15 @@ type DbUser = { password_hash: string } +type AccountWithCredentialRow = RowDataPacket & { + id: number + username: string + email: string + first_name: string + last_name: string + pw_hash: Buffer | string +} + function resolveDbPassword(): string { const inlinePassword = process.env.DB_PASSWORD if (inlinePassword) { @@ -43,12 +56,24 @@ let initPromise: Promise | null = null async function initializeSchema() { await pool.execute(` -CREATE TABLE IF NOT EXISTS auth_user ( +CREATE TABLE IF NOT EXISTS finusAccount ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - email VARCHAR(100) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + username VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_finus_account_email (email) +) +`) + + await pool.execute(` +CREATE TABLE IF NOT EXISTS credentials ( + finus_account_id INT NOT NULL, + pw_hash BLOB(256) NOT NULL, + salt CHAR(8) NOT NULL, + PRIMARY KEY (finus_account_id), + FOREIGN KEY (finus_account_id) REFERENCES finusAccount(id) ON DELETE CASCADE ) `) } @@ -77,30 +102,105 @@ async function ensureInitialized() { await initPromise } +function buildDisplayName(firstName: string, lastName: string, username: string): string { + const fullName = `${firstName} ${lastName}`.trim() + if (!fullName) { + return username + } + + return fullName +} + +function hashToString(hash: Buffer | string): string { + if (typeof hash === 'string') { + return hash + } + + return hash.toString('utf-8') +} + async function findUserByEmail(email: string): Promise { await ensureInitialized() - const [rows] = await pool.execute<(DbUser & RowDataPacket)[]>( - 'SELECT id, name, email, password_hash FROM auth_user WHERE email = ? LIMIT 1', + const [rows] = await pool.execute( + ` +SELECT + a.id, + a.username, + a.email, + a.first_name, + a.last_name, + c.pw_hash +FROM finusAccount a +JOIN credentials c ON c.finus_account_id = a.id +WHERE a.email = ? +LIMIT 1 +`, [email] ) - return rows[0] ?? null + const row = rows[0] + if (!row) { + return null + } + + return { + id: row.id, + name: buildDisplayName(row.first_name, row.last_name, row.username), + email: row.email, + password_hash: hashToString(row.pw_hash), + } } -async function createUser(name: string, email: string, passwordHash: string): Promise { +async function createUser( + username: string, + firstName: string, + lastName: string, + email: string, + passwordHash: string +): Promise { await ensureInitialized() - const [result] = await pool.execute( - 'INSERT INTO auth_user (name, email, password_hash) VALUES (?, ?, ?)', - [name, email, passwordHash] - ) - - return { - id: result.insertId, - name, - email, - password_hash: passwordHash, + const salt = Math.random().toString(36).slice(2, 10).padEnd(8, '0').slice(0, 8) + + let connection: PoolConnection | null = null + + try { + connection = await pool.getConnection() + await connection.beginTransaction() + + const [accountInsert] = await connection.execute( + ` +INSERT INTO finusAccount (username, email, first_name, last_name) +VALUES (?, ?, ?, ?) +`, + [username, email, firstName, lastName] + ) + + await connection.execute( + ` +INSERT INTO credentials (finus_account_id, pw_hash, salt) +VALUES (?, ?, ?) +`, + [accountInsert.insertId, passwordHash, salt] + ) + + await connection.commit() + + return { + id: accountInsert.insertId, + name: buildDisplayName(firstName, lastName, username), + email, + password_hash: passwordHash, + } + } catch (error) { + if (connection) { + await connection.rollback() + } + + throw error + } finally { + connection?.release() } } diff --git a/app/server/services/auth/src/index.ts b/app/server/services/auth/src/index.ts index 3e55aa8..7d7ac19 100644 --- a/app/server/services/auth/src/index.ts +++ b/app/server/services/auth/src/index.ts @@ -4,7 +4,9 @@ import { closeDatabase, createUser, findUserByEmail, type DbUser } from './db' import { hashPassword, validateEmail, validatePassword, verifyPassword } from './credentialUtils' type SignupBody = { - name?: string + username?: string + firstName?: string + lastName?: string email?: string password?: string } @@ -46,12 +48,17 @@ function issueToken(user: DbUser): string { app.post('/api/auth/signup', async (req, res) => { const body = req.body as SignupBody - const name = body.name?.trim() + const username = body.username?.trim() + const firstName = body.firstName?.trim() + const lastName = body.lastName?.trim() const email = body.email?.trim().toLowerCase() const password = body.password ?? '' - if (!name || !email || !password) { - res.status(400).json({ ok: false, error: 'Name, email and password are required.' }) + if (!username || !firstName || !lastName || !email || !password) { + res.status(400).json({ + ok: false, + error: 'Username, first name, last name, email and password are required.', + }) return } @@ -75,7 +82,7 @@ app.post('/api/auth/signup', async (req, res) => { } const passwordHash = await hashPassword(password) - const user = await createUser(name, email, passwordHash) + const user = await createUser(username, firstName, lastName, email, passwordHash) const token = issueToken(user) res.status(201).json({ diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql index affa45b..f3794eb 100644 --- a/app/server/services/database/schema.sql +++ b/app/server/services/database/schema.sql @@ -1,22 +1,22 @@ -CREATE DATABASE finus; +CREATE DATABASE IF NOT EXISTS finus; -CREATE TABLE finus.account ( +CREATE TABLE finus.finusAccount ( id INTEGER NOT NULL AUTO_INCREMENT, username VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, - age INTEGER NOT NULL, created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id) + PRIMARY KEY (id), + UNIQUE KEY uq_finus_account_email (email) ); CREATE TABLE finus.credentials ( - account_id INTEGER NOT NULL, - pw_hash BLOB(256) NOT NULL, - salt CHAR(8) NOT NULL, - PRIMARY KEY (account_id), - FOREIGN KEY (account_id) REFERENCES finus.account(id) ON DELETE CASCADE + finus_account_id INTEGER NOT NULL, + pw_hash BLOB(256) NOT NULL, + salt CHAR(8) NOT NULL, + PRIMARY KEY (finus_account_id), + FOREIGN KEY (finus_account_id) REFERENCES finus.finusAccount(id) ON DELETE CASCADE ); From 0ad04c7614f170d7be3ab29d9b530e2c53014a90 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Tue, 10 Feb 2026 21:35:08 -0600 Subject: [PATCH 033/204] Client: Introduced transaction table and animation for cards --- app/client/src/components/AccountCard.tsx | 2 +- .../src/components/TransactionTable.tsx | 116 +++++++++++++++++- app/client/src/pages/DashboardPage.tsx | 2 + app/client/src/types/Transaction.tsx | 2 + 4 files changed, 120 insertions(+), 2 deletions(-) diff --git a/app/client/src/components/AccountCard.tsx b/app/client/src/components/AccountCard.tsx index 519ec3b..56a9db1 100644 --- a/app/client/src/components/AccountCard.tsx +++ b/app/client/src/components/AccountCard.tsx @@ -6,7 +6,7 @@ type AccountCardProps = { function AccountCard(props: AccountCardProps) { const { title, amount, backgroundColor } = props; return ( -
+

{title}

{amount}

diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index fb52a3e..806ab86 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -1,8 +1,122 @@ +import type { Transaction } from '@/types/Transaction'; import React from 'react' +const transactions: Transaction[] = [ + { + id: "tx001", + amount: 1200, + category: "Income", + date: "2026-02-01", + from: "Tech Corp Inc.", + to: "Huy Truong", + description: "Monthly salary payment" + }, + { + id: "tx002", + amount: -75.5, + category: "Expenses", + date: "2026-02-02", + from: "Huy Truong", + to: "Walmart", + description: "Weekly grocery shopping" + }, + { + id: "tx003", + amount: -45, + category: "Expenses", + date: "2026-02-03", + from: "Huy Truong", + to: "City Bus Service", + description: "Monthly bus pass" + }, + { + id: "tx004", + amount: -120, + category: "Expenses", + date: "2026-02-04", + from: "Huy Truong", + to: "Hydro Company", + description: "Electricity bill" + }, + { + id: "tx005", + amount: -60, + category: "Expenses", + date: "2026-02-05", + from: "Huy Truong", + to: "Rogers", + description: "Home internet bill" + }, + { + id: "tx006", + amount: -35.75, + category: "Expenses", + date: "2026-02-06", + from: "Huy Truong", + to: "Tim Hortons", + description: "Lunch with friends" + }, + { + id: "tx007", + amount: -15.99, + category: "Expenses", + date: "2026-02-07", + from: "Huy Truong", + to: "Netflix", + description: "Monthly subscription" + }, + { + id: "tx008", + amount: -200, + category: "Expenses", + date: "2026-02-08", + from: "Huy Truong", + to: "Amazon", + description: "Electronics purchase" + }, + { + id: "tx009", + amount: 150, + category: "Expenses", + date: "2026-02-09", + from: "Client ABC", + to: "Huy Truong", + description: "Website development payment" + }, + { + id: "tx010", + amount: -500, + category: "Expenses", + date: "2026-02-01", + from: "Huy Truong", + to: "Landlord", + description: "Monthly rent" + } +]; function TransactionTable() { return ( -
TransactionTable
+
+ +
Date
+
Description
+
Category
+
Amount
+
From
+
To
+
+ {transactions.map((tx) => ( + +
{new Date(tx.date).toLocaleDateString()}
+
{tx.description || "N/A"}
+
{tx.category}
+
+ {tx.amount < 0 ? `-$${Math.abs(tx.amount).toFixed(2)}` : `$${tx.amount.toFixed(2)}`} +
+
{tx.from}
+
{tx.to}
+
+ ))} +
) } diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 49849e0..070688e 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -3,6 +3,7 @@ import type { Transaction } from '@/types/Transaction' import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; import { Pie, Line } from 'react-chartjs-2'; import AccountCard from '@/components/AccountCard'; +import TransactionTable from '@/components/TransactionTable'; Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); function DashboardPage() { const data = { @@ -106,6 +107,7 @@ function DashboardPage() {

Recent Transactions

+
) } diff --git a/app/client/src/types/Transaction.tsx b/app/client/src/types/Transaction.tsx index ef19ba5..c4dc108 100644 --- a/app/client/src/types/Transaction.tsx +++ b/app/client/src/types/Transaction.tsx @@ -3,5 +3,7 @@ export type Transaction = { amount: number; category: string; date: string; // ISO format date string + from: string; + to: string; description?: string; }; \ No newline at end of file From 587ad90845f4a5020b15cb2bc1d11a85fad0011b Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 11 Feb 2026 13:43:48 -0600 Subject: [PATCH 034/204] reworked project structure to use package & eslint inheritance + commons folder where ts services can share code --- app/server/docker-compose.yml | 94 ++++---- app/server/services/.gitignore | 175 ++++++++++++++ app/server/services/api-gateway/Dockerfile | 12 - app/server/services/api-gateway/bun.lock | 216 ----------------- app/server/services/api-gateway/bun.lockb | Bin 121512 -> 0 bytes .../services/api-gateway/eslint.config.mts | 11 - app/server/services/api-gateway/package.json | 28 --- app/server/services/api-gateway/tsconfig.json | 29 --- app/server/services/auth/Dockerfile | 12 - app/server/services/auth/bun.lock | 223 ------------------ app/server/services/auth/bun.lockb | Bin 123198 -> 0 bytes app/server/services/auth/eslint.config.ts | 11 - app/server/services/auth/package.json | 34 --- app/server/services/auth/tsconfig.json | 29 --- app/server/services/database/schema.sql | 22 -- .../{ => python}/analytics/Dockerfile | 0 .../{ => python}/analytics/requirements.txt | 0 .../{ => python}/analytics/src/main.py | 0 .../services/{ => python}/market/Dockerfile | 0 .../{ => python}/market/requirements.txt | 0 .../services/{ => python}/market/src/main.py | 0 app/server/services/ts/.gitignore | 175 ++++++++++++++ .../services/{ => ts}/api-gateway/.gitignore | 0 app/server/services/ts/api-gateway/Dockerfile | 15 ++ .../services/{ => ts}/api-gateway/README.md | 0 .../services/ts/api-gateway/package.json | 9 + .../{ => ts}/api-gateway/src/index.ts | 2 +- app/server/services/{ => ts}/auth/.gitignore | 0 app/server/services/ts/auth/Dockerfile | 14 ++ app/server/services/{ => ts}/auth/README.md | 0 app/server/services/ts/auth/bun.lockb | Bin 0 -> 6418 bytes app/server/services/ts/auth/package.json | 12 + .../{ => ts}/auth/src/credentialUtils.ts | 0 .../services/{ => ts}/auth/src/index.ts | 1 - app/server/services/ts/bun.lockb | Bin 0 -> 72207 bytes app/server/services/ts/common/port.ts | 1 + app/server/services/ts/eslint.config.mts | 13 + app/server/services/ts/package.json | 20 ++ app/server/services/ts/tsconfig.json | 48 ++++ app/server/services/{ => ts}/user/.gitignore | 0 app/server/services/ts/user/Dockerfile | 14 ++ app/server/services/{ => ts}/user/README.md | 0 app/server/services/ts/user/package.json | 9 + .../services/{ => ts}/user/src/index.ts | 2 +- app/server/services/user/Dockerfile | 12 - app/server/services/user/bun.lock | 179 -------------- app/server/services/user/bun.lockb | Bin 115888 -> 0 bytes app/server/services/user/eslint.config.ts | 11 - app/server/services/user/package.json | 30 --- app/server/services/user/tsconfig.json | 29 --- 50 files changed, 557 insertions(+), 935 deletions(-) create mode 100644 app/server/services/.gitignore delete mode 100644 app/server/services/api-gateway/Dockerfile delete mode 100644 app/server/services/api-gateway/bun.lock delete mode 100755 app/server/services/api-gateway/bun.lockb delete mode 100644 app/server/services/api-gateway/eslint.config.mts delete mode 100644 app/server/services/api-gateway/package.json delete mode 100644 app/server/services/api-gateway/tsconfig.json delete mode 100644 app/server/services/auth/Dockerfile delete mode 100644 app/server/services/auth/bun.lock delete mode 100755 app/server/services/auth/bun.lockb delete mode 100644 app/server/services/auth/eslint.config.ts delete mode 100644 app/server/services/auth/package.json delete mode 100644 app/server/services/auth/tsconfig.json delete mode 100644 app/server/services/database/schema.sql rename app/server/services/{ => python}/analytics/Dockerfile (100%) rename app/server/services/{ => python}/analytics/requirements.txt (100%) rename app/server/services/{ => python}/analytics/src/main.py (100%) rename app/server/services/{ => python}/market/Dockerfile (100%) rename app/server/services/{ => python}/market/requirements.txt (100%) rename app/server/services/{ => python}/market/src/main.py (100%) create mode 100644 app/server/services/ts/.gitignore rename app/server/services/{ => ts}/api-gateway/.gitignore (100%) create mode 100644 app/server/services/ts/api-gateway/Dockerfile rename app/server/services/{ => ts}/api-gateway/README.md (100%) create mode 100644 app/server/services/ts/api-gateway/package.json rename app/server/services/{ => ts}/api-gateway/src/index.ts (97%) rename app/server/services/{ => ts}/auth/.gitignore (100%) create mode 100644 app/server/services/ts/auth/Dockerfile rename app/server/services/{ => ts}/auth/README.md (100%) create mode 100755 app/server/services/ts/auth/bun.lockb create mode 100644 app/server/services/ts/auth/package.json rename app/server/services/{ => ts}/auth/src/credentialUtils.ts (100%) rename app/server/services/{ => ts}/auth/src/index.ts (98%) create mode 100755 app/server/services/ts/bun.lockb create mode 100644 app/server/services/ts/common/port.ts create mode 100644 app/server/services/ts/eslint.config.mts create mode 100644 app/server/services/ts/package.json create mode 100644 app/server/services/ts/tsconfig.json rename app/server/services/{ => ts}/user/.gitignore (100%) create mode 100644 app/server/services/ts/user/Dockerfile rename app/server/services/{ => ts}/user/README.md (100%) create mode 100644 app/server/services/ts/user/package.json rename app/server/services/{ => ts}/user/src/index.ts (89%) delete mode 100644 app/server/services/user/Dockerfile delete mode 100644 app/server/services/user/bun.lock delete mode 100755 app/server/services/user/bun.lockb delete mode 100644 app/server/services/user/eslint.config.ts delete mode 100644 app/server/services/user/package.json delete mode 100644 app/server/services/user/tsconfig.json diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 5b1aa57..f1bd78c 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -1,53 +1,59 @@ services: - api-gateway: - build: ./services/api-gateway - ports: - - "3000:8000" - env_file: - - path: ./default.env - environment: - - NODE_ENV=development - depends_on: - - auth - - user - - analytics - - market - - auth: - build: ./services/auth - env_file: - - path: ./default.env - - user: - build: ./services/user - env_file: - - path: ./default.env + api-gateway: + build: + context: services/ts + dockerfile: api-gateway/Dockerfile + env_file: + - path: ./default.env + environment: + - NODE_ENV=development + ports: + - "3000:8000" + depends_on: + - auth + - user + - analytics + - market - analytics: - build: ./services/analytics + auth: + build: + context: services/ts + dockerfile: auth/Dockerfile + env_file: + - path: ./default.env + + user: + build: + context: services/ts + dockerfile: user/Dockerfile env_file: - path: ./default.env - market: - build: ./services/market - env_file: - - path: ./default.env - - #connect manually with `mysql -h 127.0.0.1 -P 3306 -u root -p` - database: - image: mysql:8.0-debian - volumes: - - db_data:/var/lib/mysql - - ./services/database:/docker-entrypoint-initdb.d - secrets: - - db_root_password - - db_password - environment: - - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password - - MYSQL_PASSWORD_FILE=/run/secrets/db_password - ports: - - "3306:3306" + analytics: + build: ./services/python/analytics + env_file: + - path: ./default.env + + market: + build: ./services/python/market + env_file: + - path: ./default.env + + #connect manually with `mysql -h 127.0.0.1 -P 3306 -u root -p` + database: + image: mysql:8.0-debian + volumes: + - db_data:/var/lib/mysql + - ./services/database:/docker-entrypoint-initdb.d + secrets: + - db_root_password + - db_password + environment: + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password + - MYSQL_PASSWORD_FILE=/run/secrets/db_password + ports: + - "3306:3306" secrets: diff --git a/app/server/services/.gitignore b/app/server/services/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/app/server/services/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/app/server/services/api-gateway/Dockerfile b/app/server/services/api-gateway/Dockerfile deleted file mode 100644 index 952e6dc..0000000 --- a/app/server/services/api-gateway/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM oven/bun:1.0-slim - -WORKDIR /app - -COPY package.json . -RUN bun install - -COPY . . - -EXPOSE 3000 - -CMD ["bun", "src/index.ts"] diff --git a/app/server/services/api-gateway/bun.lock b/app/server/services/api-gateway/bun.lock deleted file mode 100644 index 8bff853..0000000 --- a/app/server/services/api-gateway/bun.lock +++ /dev/null @@ -1,216 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "api-gateway-s", - "dependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "cors": "^2.8.6", - "express": "^5.2.1", - "http-proxy-middleware": "^3.0.5", - }, - "devDependencies": { - "@types/bun": "latest", - "@types/node": "^25.2.0", - "typescript": "^5.3.3", - }, - }, - }, - "packages": { - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - - "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="], - - "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], - - "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], - - "http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - } -} diff --git a/app/server/services/api-gateway/bun.lockb b/app/server/services/api-gateway/bun.lockb deleted file mode 100755 index 86441aba1c4bd6be1a42adb80407cd27aeab9172..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121512 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

p$!RQvTn#unuYW4e;Uld$VyvbP+xuc-aisAMF31 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 COby2X diff --git a/app/server/services/api-gateway/eslint.config.mts b/app/server/services/api-gateway/eslint.config.mts deleted file mode 100644 index 673b075..0000000 --- a/app/server/services/api-gateway/eslint.config.mts +++ /dev/null @@ -1,11 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import tseslint from "typescript-eslint"; -import pluginReact from "eslint-plugin-react"; -import { defineConfig } from "eslint/config"; - -export default defineConfig([ - { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } }, - tseslint.configs.recommended, - pluginReact.configs.flat.recommended, -]); diff --git a/app/server/services/api-gateway/package.json b/app/server/services/api-gateway/package.json deleted file mode 100644 index d646ccb..0000000 --- a/app/server/services/api-gateway/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "api-gateway-s", - "scripts": { - "dev": "bun --watch src/index.ts", - "build": "tsc", - "start": "bun dist/index.js" - }, - "dependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "cors": "^2.8.6", - "eslint": "^9.39.2", - "express": "^5.2.1", - "http-proxy-middleware": "^3.0.5" - }, - "devDependencies": { - "@eslint/js": "^9.39.2", - "@types/bun": "latest", - "@types/node": "^25.2.0", - "@typescript-eslint/eslint-plugin": "^8.54.0", - "@typescript-eslint/parser": "^8.54.0", - "eslint-plugin-react": "^7.37.5", - "globals": "^17.3.0", - "jiti": "^2.6.1", - "typescript": "^5.3.3", - "typescript-eslint": "^8.54.0" - } -} diff --git a/app/server/services/api-gateway/tsconfig.json b/app/server/services/api-gateway/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/app/server/services/api-gateway/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/app/server/services/auth/Dockerfile b/app/server/services/auth/Dockerfile deleted file mode 100644 index 136cfb3..0000000 --- a/app/server/services/auth/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM oven/bun:1.0-slim - -WORKDIR /app - -COPY package.json ./ -RUN bun install - -COPY . . - -EXPOSE 3001 - -CMD ["bun", "src/index.ts"] \ No newline at end of file diff --git a/app/server/services/auth/bun.lock b/app/server/services/auth/bun.lock deleted file mode 100644 index f40f72b..0000000 --- a/app/server/services/auth/bun.lock +++ /dev/null @@ -1,223 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "auth-s", - "dependencies": { - "bcrypt": "^6.0.0", - "express": "^5.2.1", - "jsonwebtoken": "^9.0.3", - }, - "devDependencies": { - "@types/bcrypt": "^6.0.0", - "@types/bun": "latest", - "@types/express": "^5.0.6", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^25.1.0", - }, - "peerDependencies": { - "typescript": "^5.9.3", - }, - }, - }, - "packages": { - "@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="], - - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], - - "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], - - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], - - "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - - "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], - - "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], - - "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], - - "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], - - "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], - - "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], - - "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - } -} diff --git a/app/server/services/auth/bun.lockb b/app/server/services/auth/bun.lockb deleted file mode 100755 index d4d299274cf2d6b565cf0a5d51e3bb15101121f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123198 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%0ATG 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≦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 literal 0 HcmV?d00001 diff --git a/app/server/services/ts/auth/package.json b/app/server/services/ts/auth/package.json new file mode 100644 index 0000000..7e48891 --- /dev/null +++ b/app/server/services/ts/auth/package.json @@ -0,0 +1,12 @@ +{ + "name": "auth", + "main": "src/index.ts", + "dependencies": { + "jsonwebtoken": "^9.0.3" + }, + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "tsc", + "start": "bun dist/index.js" + } +} diff --git a/app/server/services/auth/src/credentialUtils.ts b/app/server/services/ts/auth/src/credentialUtils.ts similarity index 100% rename from app/server/services/auth/src/credentialUtils.ts rename to app/server/services/ts/auth/src/credentialUtils.ts diff --git a/app/server/services/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts similarity index 98% rename from app/server/services/auth/src/index.ts rename to app/server/services/ts/auth/src/index.ts index 35bc14f..a27de80 100644 --- a/app/server/services/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -1,6 +1,5 @@ import { stmts } from "./db"; -const PORT = process.env.PORT || 8000; function json(body: unknown, status = 200) { return new Response(JSON.stringify(body), { diff --git a/app/server/services/ts/bun.lockb b/app/server/services/ts/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..a760a39cb2ef01e1be48b9e7250a248a0a45ab10 GIT binary patch literal 72207 zcmeEvcRZKv`?tMiL_$_ZMs^BW$)1%JlKHW+S6SIBDl1zN$yPo^?{oc3k`}e$F&mSGHPRDhQ_xm``;~du{b~ZszcQ-)`J10SmlNYOnrxOWC z{0`2R=dA4gYISXhC(fTjX^3$&mF z`ZLgEK)Yj{tRTqU!`#Eu9pnCPySxo(Qc#ZpF59`edpNqoR#;e9Kq3LZDq?p7F=ANR4?SkRAbH}EvSP zg2BSF@b$KL(7!`VOFJfUX7_#v=)6=(i{!AqUzF7^4H49cY-p4C=wZKrN7` z23i(q3ZR9y^#ls!z0m#F8H)l@|j2jk~I&c>Hc?@V+Zt3iN&JOg`%iPTyP+>VcS%MrE z)_FH`7Z*@s>FoBS=TCt#*bf;{2kpW(5RY+puygXj!rBDE-~{%;j)idt z>j~sDSbo-$=lM|3WE(Q zVfhrOhxUfHb|0Ps{oY4d3JqU+qv^Y7qLn>ezr^4E*A!a z3GIXf4ehv?d)V@WAf30hvxMu=&K+|BbO$R6w1?yHgzINq9{>&QSYa?OaGim%`%!Og z=VZn2>4LS3`=@=}ZS4ZLHggZq*ul=x&I1dV=Vv{}-P6PFN8DUJF<@$1+L}9@16*G~ zJsfYa9{9o7+01OWzt8*APcpur_8Pz>09^`XK8S?{LKFB6l(PZd2{f2efnz{}p$M!4 z8pf*(XxI;8fuH_cTiJotf`#?xdFPIC^n&Xs1Mstg_8~xnDHvEL_%ojXG?+4h=Ri5M zgUkbj$KKAv4)P-N0+{__msw_=dd4bGtWc?uV1-}2l*&O;g z4mCi-adWhC;; zS?#hTyZ7GkO}(72E0V<7h4G};X%moY!%Sb&(I?D^d&C~{?xu^&u03N?qFWVr2ohch zD@A{kJ=tAro1Cg58%-Rg_WDUxQo+8FXQp9ZeX-dZ+Ap3vw&z@w9UD6NzGzCvsUnre zcX>b}pvdL)i)KQF_iMF@ZusZkP0Pi8J-3!IKWsR%PU>Y7&_BM#hg0ymCuz?Zetf{sYlDuVp`TbW1p8FfKq>QSHIi2MtlB|Y zjpsKw0}t|rVJzSy-pz@UEQ)RcLu(~co(0H+XgI-Z74#lVUrQUG{6RxXo zB42rwYxNMF*J~=gM%Y(OPhh~o$nBNb_2E#lXW7`?mFx<=Z~lFH%P-k!vkN9=$Jpqp zNu+n-sE-7(OB1fCvfxadJ-M*pV%W5RpD{Mi-F?|_7adOj1%s0>G)+Etzd`J{Yn;d3=vQJ83h+u9{h&(9Cii(MIxrdZ1<#=)Uj=+|!@>m0jV zL}7XAR@U5Ci&4|vM2CvlPTyP)`_3^-#!62b%qcQ+sl`SSuhe!u>3dPSJi( zL!?MbqFnon+S7Z7SFc*tWxf6MMLThq+=$w-P}vuZYEQb9haAT{HN5jgB z4v+_Sp5eO`&Ld+Hbz~R52`y!Lz0Xyr6q&O)4KEgn#*HG_d?cz@I-Zffv+zyeZ*f|a z_^N4tiZMNvqfs;A_VPLD#7NiYBM;15**cU{=)VY35LvLqRnA-wuRosuZCBp;Q0sRB z+MZ%0nQvZ(5>ASqxaiO@QeIgsM?0P(*+k?|^1*;fHY;ZRnS0Y#|0yZrM~+ukbF@Dz zB|eBs`gBlwc4Ma{e37bsU+eHv!(87iQsqf%i3| zUvsG#Qw2<;UHuh`D+jF7Z%3bNOr?I_K4EIif)n>j6}#f8Sp?2VxL- zWh?af#iyK@mzw&AI3(g~+S=^)3r7-a`JQ*-S&&EO0m2iNF<^kFPu7ggiyi`=`|3hL z%f53qy^++j`{M+SE^}2&OAtFtbIb%2-IwvPu4|EmAuxIg`eI3)dw|1JOzF4caF zKg5IVjs)p{1^`F_Jc2{Y|BeJ{Hw1u3w&MrZ!HxyNQ-Gi$@q^%Bjh_v`%Wd2LC-~a{ zudt0re1bfGCrH~_fJe_iklm3Wcy_RH!1<5lcLWBFkoGzN4=#ECF+ObDK8!mNNWU8Y zYJi91hv1NT^mqM0{9gii$$#NLKf%xb!??rrtM*L+9*!UMA7XzMpA7JF+x|liBo5&4 zF9qrU;5Ht?fpF|d5Ih}t{(`I@Sca7U9SPFT6yV|c3&|sX>=d81ZU0yL4!IF~V*n51 zkHj4*->LmQVA8_<3$_PTfAGTmUkPHz3E<^WHSG6(<4Z=?>fe;zz^&`0@?lq2o_ zn?~C80z6{>pNu~Rc-R5=Pl){|{;LAK2Fm|mjsG=(SK7w!6boa5^#9eL@H^!|@N8hv zk@L?^v1t3I0FT5EmhY4Uv40ET<^DzdrT`v&{R@5Ai6HiA!J>!z50Xdp-*F)AQ~@6D zpGd!db^U|^JiPxv@PB6)sYmShZQDoka1Y;!AnlpJ!&YScz*h9j<7cpqhc*$;U$Gwt z@aXgBPOxZOPXQhs|6j$EQ~uBR{}cZW0bT)&A2<#Dqx~Zj;GzFJoi|AT5MPJ4{fFiM zcQt%!Fjz7F^h3jvp=O6^H3-I6>e;~vj z00}7vhkq$ZJ2vnj-woiAek1xX!JrbsCjh+Ozrare{3(EkbLXGzKYDw9KL3GngB-sa zzZ`&9-L?--rGNAb9R8&s{htGPc>aftp?#zr=Ko5NcH-bYq{KEJOvN1wf)4?Bc>RFv z`(QiVks$bPfIk87(Dz>v1W&&g3+vdn|NkWZMgR}v4{d{~@XPD37U1Fe6Z!+^E)oZD z_?LqCj{{z!!Sf%SHxQ4M|0NjJP95Nt0Ulyuf^%pmg4ihpc)0!%4CL5}AozCxudr<& z;*j#c<3QT+GW=XWaO_~&uiC!=@X$WA2RVKf-vIFN{PR!oSm1p+5czFK>_y1oJ#D34VeZ==)jUU0jpVv=t{vv57ejxUx03OC4+K1;L#E-w@K=9`Q z9^OA7`|hv$Uk&hb+xUOtKMi>4t+kDZwtv;W3&6wcPuTB&g3kkZ`1}Ql$FJHS19)`* ze-(d(`6qs-;|99#jg>8}v(p;B5e24B)|I46I*?A%edJ@UZ{?)PEdSEG+Pe#gF}C zr{`|iQN+F&zzYIAyzYhPo?pHGjR*KM01x?gI`*LHANCgj9$r7dGGqD$}AKs6Jbrkpy@lbYp z9zpP~01xLM#8QIvtJgoZC_K28*zp`d?0*J$Iba|59nL+Z931|oAniohf8t@k|4ID9 z0A32%N7mn88bwti_FDiR#t+(t{D{}zBOK|+eBkL9^|2sdBcIN>e zIe$USPWyo1YXBa`59Xo&$UKBxeN#kMRGU zM*6>t=l|aS{tk!KBX|vfhxU>DPS-KgK6o1s=MEu4z5|WmYXKg2foSMbUJ56ADH&Yu8)hvy%N-)Zcie~7O=051>lNFK&uCxW!!!}oLlfjF3U z+7ASO3gFKI`$PZ>uH$zs2wv_W7M9V!z^4KH>3@O$4Dd?-0x!;wg{Av1@G$@{2kXIG7`T>051*jNZC&BK@dE(!2eu7L5`iyK?JV>@bLKq-2Z-M4@2;N+xCf}aUeV8 zK=5|~9*!T9NAyp&|NawcM+|Ph;Qa?&|1j-z3=q6D!0QA2Kb`+i0AA%^;P-;y{~P;G z0Dt0N*lz^*f3tq@g#Rc0aI}7P{KWwtxqpZI7o3MX5oG*=0UmVW$NYiVU)?_|03JTS zpaR&Pt{vzfV*dlc!|NYt7vhn10NebPAov5|@X^cyA;q`}{41RntK3IGqA!@0LpKM;Hy zz{B`K+)lC37J}aZc;xvNEZ->yf)@gt7o2|(yXzm|&jY;NcKqP{hx^!01hL-^@bcUJ zhh@L&KLO~x1i(Wq-A)Xn+Q8>!Fzo}0d%OHUX}G6@(>(k=r$4SykZTpFq0e9pe+XK` za^Tw!ff~*aaIN-3{*8vbvfDgR!?-JM=b?ssfEGw_KA!{$3e>P4dLThO*z-f&w*ME6yOn*t>!R<*zj4b^Fokr-nEWkf5LEx67f1?Jj@>>wUJh z-?qL8G!(Rk?E+yTkU)^2-(es@`6~_Wg#T#uQ$wD}?L5>lu2;76P_u(nzMV&Fh^yQ# zM{CGm1rpTN+x7oR!~WN9*Fz24-vbHq)`JA)KWSKhA0()oK!Uhtkf1b!1oJH*!TL6k zpg;}F+hGO>)NsD^!VD0oVf|y60RlC|KYi~|5qCNv#{L`YRI#=tv_z-Pe4OKYuN5HND#ja63l-A2@2FOzp|}YfrbJ# zT!+}862=!FXxNSb{@B)#k7&D`7-*O$-L8ik9+GWq@@)-&?;Z-&aNmQ!&=1eitUyEj z{_Q*u&@e87Ktq8V9tv&e(Ha&BZ`Z@$+ktse_+wi`|Br0vp@w!OfQChq+xjTbaNJ~o zhV^o=5~blW+|yy&2hxAq&q48za%k@$NWb^>-}`zwoD@LN8qP2Deh%wl9!|O6`#Oxq z?|uFEzW#GP{J-DVwQAy4xGy%><;m#Pu4n6? zec|dtjgjA>$`_fFSpE=d;U5%zgZ{y>br(_i|Sv+0k;gYUS zT^*^nUUF)>^Eb$Qd_wWUb08vkYTic%!@SS2sfTBd$0;b>YfQQ;WGemSs^zKoOW(Qb ze532Q*vhcX@b!vwgb2QN#5kT5bRFbz5L&oZ)Nh(A*nZMS5*a)UQLVh@#Eu4^ekm# z>n1i&vdh0%Ec=pwh;3ZTfx!;PTpPs;uRRdK8z3GIDC=+w-e-SkQk&>wt@u*C425*( zVYO#hlUf3go2IZh5-A$B*m)$S4o$a+_|&(bd9E{U_9nt)=}T$^y%>rYUh5!&x9gM0 z`)lXPm>1$bY&RmW(94+08gnW?qP$6K5`Q7fYKceWp%HiDo(Q58Jn)A}JUL$`zQ-2N z4&8G=Y0rs$W){jQULu4PXgucHgAZo-3S+Xa+bTNbMeM$Pc=hH*CLzkRF$#3~Ywj5bve zw~RkIAtO4x?__K0KKiRXWt?Y-!!?Uy_Gan@9(0#+s(0c#a@B|nf0M_$R&tYs`C>eZ z7yUd0@AMdd6y^W+t z^OB_9XZ>VJc-GhMU#WJg;>@TLvj?^Qjta_kF%&amwLf${Rpl*bMI-pX1@%|7P-%}xMIyJXRJ}ZUYcwZ zNAr&lw(@w^I`h+T*aFepVd_Bh~?>_c^->~BY#186C9yiXam!I2P z#g`Sucj_&QmmDDl8qZEkfop<%*lY~EeY}0H1AjdrJqRnz!sf(__%@aD%BHs_H9j)C zYN;KFV+R#OSs08)9azi~&Q^TN`5H)XGA)hb-G%0Tt^d>mf6MWJbxt77p0`z}hpzUu zne7=b`Xp{9W_PRf5POa8Q4Gt)UWqfl&JwH4B&t3i_jg{`%9`dOE|Kj~Pet)kpm}rB zsim?xLOyoM4M$9RdThNf5~y?6dvA4$g@5m>m)<_6d2{SMAEwk|0)`e_wDfhWlp8sm z=B6SQ`bwl^P7o@fc+t=3@OH#D$9DV>GBHExviFw7zw=4C`$PA-;{`r9%6gu+eFEQPx7g##kzbWci7N z7c;BxjChevKAlsq@Q25V^p8k6xVsF8?u2ISjTas1R13zx&||>2r`9tfm5t!Sq&Umv}xGxV^VO#}Heh8x<9v@OG;Xr*~l0FVbglLpEcBMY-@m*CP%hya34) zHC>cdi3)d4h2r5nB)Ws*-Hql|W*O-2^m*c*mR}JVeVb6>X5^xjzC-305&HSG5wh&u zF}mswmLYS38-i?yW1c;gAp?GP~ytct>bjG9;VX zP-^Wzye^in@JXPuYwNOvV8eRwm2pyX)oBGThf@5M%s};fth%N>RM$%V=tfC&_Q883 ztDI&dRN*hix(S+~3gacn_NQxav2iy|w8#o%V)#UNE$7i1oEnbgdulDt>fR zq$T*7(IbwIN+mL%)Aps^%d8mv)q$@{LEhsjOPXgLY$O$qm7@HmL-Q&)2pRcNHY%Fh zf4)}Z7`C@TrXw`-5uQuB+QPKBjxPB^s+KvXx52=B*Th3Ji-sbtQy=QS5-r=_ykaaD zGcb~m;@ykpB{_KgD3#7;lG~Bgs=nvrrn{(#sD0GAMPEo_o|EJxQMT_j(|%z$ck(3Z zv+FhCLDl|UtA2jB&kAaqEgU#Ku~mTLrAPCsx8_^p86J6euXu!4o8AyJIjOz1@46$E z#Mx#3_a~V$c|I?iDg-WvUZa;!B@vXI{#f&l^r7%8uc4WK&Cc&U@SYYKHwH9srudA1 zvm!OGny1U1H-S%1A9&|-S1r94<6**sb4jO(w}W1PL^9ojdKkQJVa; zw~tSssy+P7MI6NoK0E$H@Uph_V)1$sNA?A8T0C1ibo=A#4YL|5x)i;D5V2?pt;32p z)+uXjwy^xYv8FTCeQ1WzJeGPtW_Vu@$mzKFQ? znwnZis;|-}fw`rxMN`7?`b73HslUHu>2Tn)abSdV`)FUJh{P`u24u!4P^FXKs!1g67|W&XM=XTQfL(;khNNo# zyuVNMQA!btmle%xo_2(b#!XSJUbbwQM~ME-q-Rq8`PLjM_8RnAc zjNa>*ziofg8h!7sCvH%^o2c@!U|WS<{o%5k#=IA^3znWTPro=qwiY1x(XrMBeSYIa z^PXOAV*d0*GwQ*m*6HO3ym;ruvBObGuhpp3C}@yqpnzke2wy$9uO0 z3lHO~44ZG-pLoUVgOu<;$P!3u={L8E1UGN-1#t%%pwIIM(7eW_69Q+6hZNiC5077P zxpA?!?ING^L0q$^ss`iOx+gh4ty&~27-R`~(gyK{ZjipguB|o7?+h6f?P>2HQZx2N z#TR)V2kG&?Ea#{e9{jefqI>CL$9~N20@GG1>bXiso~$6Yf`|&qmCTZ3kyxrSm$@Xx zmp)n?Ke?wUg}t*N|5`}ujmu4b@L47@&v?-Os%08Eo$|Q!@vXY}h;6bf^@rZ~Ul!wA zRA07X)2`P&Q2QK)A28h1(`<0Jzl|rwS6hNR=i?p!?|3(|)DtA`mHxb!{dr%=i{_no z?QOp}V>+Vr&O)eD#VJ)%c(avCtSxI-gMQgVuUwLqQS%SeJx>$II`G--NYb{lb*ktZ z#iLZHmh8HPKiyS9`OAmq#cU}L6+6D#`}}@|v#hoLL3y0^T{-KNT%sbJJsGtf&1M6i zuiapgHs&XA6wD1s79u{W*!v_b0`rVWN0(n9qY}jnpJyV1=j;9SBH;_0OTByV*Sy4M zHPwnea`1Ye7KxYeIl77GN+HQnVW-}b1VDa+AQC}*Atk0%Ga^>yv%&I8eLFWSr&%LGYdtRC_WUG%+#3{*UOAkJb zx9Pl<{0e`hBTQ;gnyqISX-J4)_ISHt!84S_$isBVP^J3)V zBJR9BVT2VW#IG4LeBgr1Dz58;*D83^IoD6#m+|Q?J3xJ<*X1GWdPAC##n6FAl0D`$ zg#qqYKG3~f^>a`{@rt5(pHb_6VZGXDDauM`xF<*3#JbKa-`DWzvP-b^!`Bt+J)L82 zUq8}umfTTJ$=B06_F39edFsk13M}mTn%8O)H0bqq2+g~}C%sxNJ(DKR$I?s?D_=+{ zO+BY-haV!pYh^X=Ybb+xlN)`A(llOCnML{qa~~h!>vE}y2Xr>vge6Yi%DsLC<*yi; zmuCNiYRd1yKJt-l?UC`1TZY4A=R`@&+T;4%=3edXcypn?%(24Xu%1YW!$Bn_Dk7>? zuDJ56vPSZo-b*Bvd>Sa;!)V?l-Gf3^iR8zhPezWGXg?mWyLJ7*M3v~44avLIWNww> zXx3c%eox?S8b6kZ__O{vXa>t%tB};QU^XSoQ ziM|oJ<@;)fC1eke#b^(tpzi}D(7aO*Ur+O?wR~ktHA?BX*nj5sk>mPrm9nFw=aatY zp@ru-pzAK%Yy;!{Sn zF;MkP6=i6d%Ehko>d+-)7Q1s2+f2nvqyi+(G(4!}KDxTBDOANA>Sl1Md}sUJXKTvJ z_+fnw1Bw^?&cz>smr*r)qTsF$cl5$NbNlor>$4JcxMp#2X(UO;tX?_fkwS^FR~}X6 zuC-F&_~I3qdcU>pI+)ty_yT9ZQ@d{(%N)fk^#?21&lZlj=!QC$eJ8U>Hc-cne;RT8 z$Hdv6T-l+jgo~$H+>B#kx536H%?(H%?p9=3wQw4`iTt!8dw; z2wwQQEqeiXDYZuC^vY85sgQz&!S{9bj7J|iwwMv$bLI`Hz|k}&a~CaMk4gy)!n>f6 zxy)szbCe`ihkPKJZ56&NM9!Nsf3O07dsOb7bt?^M^hnMWpqIeZ6p*mq70)C;dnlCQ zZdf}Zm$#?w0p7g#g;uk8TE||GO%=DQ9d{@>-IZ|E{!ml4(03HCESeYZY5M7!(Wg@s zcC}U}iq_9$1?rX+y2ZV3E*pIt$#45;k9}9QOYok*c4RMZWeQHEMIIGaXucBNi96>b zZI%WLQM__!-o$~Zz^iwUU!#(v-{&G9p^1Cd?!0cNmm#(8Zp>veS~&q*wd*gmFZX<9 z>RXBI8mhB-E_yWC`9;8i_kEp|b>}xwyvNYI$)N?Cha7w^eQRzEy>HT}_4dH*d5zew z!&^Q6XI0oVl=_zowAIh(mS-+3ex^JcBV65b>`g6?-0~S^51jjReXb~8c{FcTx*;A_ z^UT*S>}(%uH-@4DBfQ<;?C%VXMPK6*X36q;ir>SY)*#yf));8YH#QC>M zkH0acO0D~Zz8^Y{<}FUyE$>Z};qO34A)LnWPnJvHm__Shwq{7Qf(b58+RQk0qd^=&l;MhuNE#dN11AYCgh~~Xw z;%p<@cFAq_%M|~}jrsYrVS!JDz7Q?qt-4jps`+EJO&UBY=1-aU9P7oEn5+BM(c8G6 z>Wx5}U+Zw4XyR*il)p-7-h&!xX}R^raV=xh-92Bcm~fiB`c=fzN8g!Y-5yMs5?qMc z7k-6Hom6y$>CA#john&efcrkeYpcw9a>FkTpP`>CE2DYw2c{o>@U0?$Oz#wSl}*{` z_?p0Lx2w8u$K57-N*Ep-98DPi&dGhZ{(N6QUf%grms?A(iRTd{k~Yk77whw`%cA^M zLGwB@jv0jcH6<}loX6Lco|WdN$D5IlKhpDDfLl*bwK+OOmqvr!r?1;XeYJ1@RH1+R z!27Oa?1pRijO47VS0DPJcvaE7c??4-Ln(QQzK>lMyz*I2dp@0hl4~VjAAujjbdCMe z_{uj;z7M`{cbQOI_>jFnIWu=!p8i>zcJU`7_LKp-@^Tcf8k)B?S%&}o%^vRJ_BDk# zjTbWyKdT$evd%vW+55$Rq+Z5(Z-}_SIi|}a`b`uoS^f6ul4+ytvnjKTnn} zC;LD&m27C&Q1j9YLKBXr9Gi;x!jDc#>=!!o_P!5h@fhc&XrDfpFLud@C&J*9+%Wr5 zhS^&vUM)25`}chxzmw5CX~gA{r(kG#nMzOdsj>h+@p0nyx+(QWXIkMNKC$pS1cmtS z)D4bgU*$XBPlVx7eW+~~8F%L+*oWfPM)PXDJaZ^>5LfCv!<~bM`?~G)bCh}o3@8tq z5i>cTem5LN(HB(bgO88PdgMm>JZ1P}4+65K-^k`}+dK4h>nY9%4AF{c+HlDPWL!n2$>P z{J_UMQdtkyZ#IYSKVU5MU1+o5>RY8XC3GB4p?US6>^jGjq_=V=HR;2&W?ql|aaP~Y zmwjaiEoKZO``Ze0Np|>aW$6PeayROjZxyvl-MC*p*Fqx=!;u~`}rg+@0s9X^=9@M!JgeZ&6(+yhulJ$3I zWe#h{?EY@7uh5<8*o?_fzp?f~_(I|;?-iE^ZYW+oH19Xw#W$~nx^a(Pj8wWUYx^O5 zl~MXxiQEj{#dBKIO3HV$@9Gcndb+k*x6-s@E@vI3a(!uXL#XV$t%vcecSedm)~_rD%Uep4l9=Z|%R(W8bb zrZmyc7>_{~hm(zsI4n#8y-ymT{XIoz_3pup>-(nVfLWpmi@2Eeh06@XQinsWw8b4$ z`k69A?#?RCJ27gciD5mcfBSgrEd|}yS>i%xbDoG(M}0X_aX5|UoeY?{TlV6n+Qz}O zd^(#WYt#qoH!k$9e=vVZO}-)$zRT+lPX7mq$M>Js`g_;!R|p!%=8Nl&QXdKp7ju+g zV~;`c8lrjEs#a<*;=L^oU0``XIy<7iL78~1HTRNFkMpk6*UuWoBa8uoy-ge zCI8EY@$5Tma#TSPloJmOG-~W9(a+C}(7b0260`f9nlUq%9q8;7hH)Gox#)W@-Jrxh zxA;Jh?>+UtXxc8>MbeBDua#1D$JL8x1`EB6IcPT;?G{KDpXa0ZNn^I zA*O?a?@)0#gXT5yc+t!^QC@0m*QTWK{gmIU(|AwFqTgyaQ&~IdE6`Q?R$>nw6KKM$ z9aY^Y{949xgl%O|pIos=jO;kM-imxCiq{0q8&I>4=5vjztMY9o?-M~43O7193HfXD zJbH~UkFK}m1ij}w&+xEc!C1kkYXyHFKUQ$QQ|xBXm6E$PvF~ts!ylq}&!Tzh11c(a zQQ#e1mX$oX??{1_89DVEd7--ZisGRUpYLOk$R#EaQYyvzc5!psc|4|XOY-O*RZYW^ zEB0M@hAE2+ttehoG;a>Aa;!%HW%3G9|9fK^b>o7pxr|_9Wr8P16&Ng5-1@m+Ea_bG z*FME_@y_?!I|)4cSWOrGt*$)sO*>Q?Qy7W9jxs~@zNz-cU-nA7{X`?R(I8!0dT`4+ zJv%b`c`cTANcX-H_taa5z8@!}RWx&J@wa{4c#C>+)WOfiHr0CBDUbaeiz>=rb2Kk+ zQ?yhVEuVB@0+TXU%Vv>L-MPavc%*ck8po2CLUv7Qn_e+oKJt<9q4s6*x{Luog2^M9 z6|STitvI{RCb{TpqIfOPydRcE)|mH4kX)S}f4TqLjR9Q3&pOwttq5;s)pnKUtBC1k zKg`2<&{`-#cRfEvgg&N4@ExVwhk1kSq|uq=0)p&8oPt=6`#Q?( zju80iq&Big`HVh(xM|IxnPqjcer~_UviiLLi`*a^EX@?l@7Gmr)ZRVxt{|4dl%aU7 z(7a2F`UUS^(McLCzrAQ!dgawX@%~6=g-0U{Pl&``YV7k13^8GOo)k}e!q`PEJYZ?C z?Ib()jlx$e?F8g)jWa!?C|(SjSCgsdV%4x_a35j*i_4onLZ!EE;ud4)6FrA3A=fhFP)$=gDa9!tVNmb|_vuG;esbj+k1kh7^a< zAd}hUo6qxPGvyS@p5k)k4|s8t-k%ovEVsac*Km*CE?$}PHXEIwQb3;2=eI7_B8TJ; z*z%(NwMX-2=PNZ%63FX(+~Vsp+uN^J9q4b-;-h}=ZbJL;(p3S$B&O9a!}{!(PjVtS z*fe%O3W$@A5hi@ClcyQEx22-28s+afH1E}%=HurQ?=)#!Q(DT#-n&Te=Nq7yO8@vu z^ID?aK(U@ww0Zqv(J{WJ+p?{t#}uA-5$Luo^PVB%)4izS5Fn3!{@{S-ExqILHNiYU z9w+?haZ`HCfm-o_aH}aEgS(leEM$RG4{};BF7jFwOeD-k=iy!0SdrGcS4PSlR6W0+ z=N;LJ!61~sj%Z%7j41muQ^9)!&6gT!MAofueBP&WR;tY=BU5DT_QVC=cZXN@)jYg& zm-9lu`jqNIS}x(+rz>K#)#YDDSkltxwNSiHXx^*m`vpj!=VaE~`=xXxglh&HGtR!x zBoN%KdN0jxU%yD>4N9IoxoDx_WxMN4PI~f(gW|*K8xw@ZF&eo9A*|^6>x|}2q6=s? zjZPRRv<_{(t9Q=U*EHhV&?OOed%cl_dWKW4(lS{;AADrPOp{D0V$VXkk=E` z${yJFcGme=X=nPD<$fkoi&b2TjmHuSt2O$lIJlyD1AJmvLPb`j14HSWSo-Bx&yc2M z$rEe8Ci~EHh=AZ7qtMYZbzW&{rpL-=Bi~#sWH#vS$o8|6=a=aB9>G53_Xfr5hUQ(U zNPNh@di+erYR`!mLR=xMhM9iN*@L=Lt8+ zGF^E!cROPFvuEDhiix8#3T;%htp1X%^`1vxv3k8>4~o|V%`0Jf{L^%e{&V>i=Kfi= z8v!2Ivdd>z$AYzqgzwdg91G$JarTIK=HWCKO7vXSmmWVoeV4tsH=~J3@<=T)gR#x74( zb6DWG9XQ+lg^}T)nfg#~(46=6f<0@UM-5Y43AnkUCcWkTq^+Z=#ZKJk!KrY5lW6O3 z=>&@R0-E}U=fiq{*>Th{;BFYET=rTI_PINT4r)vJ39zPw$LJ4&(t z+-D1m1dn=BB9XBIt9Xa_mv6Dh#G^uPX?i9HzL;lf9(z2!POuQ@nxoI z`G+6T@8^Bdyu3C&f?n&4Zw(s0lMk$~YL!?CuH)<>UJ(mxzninUdH(}Xp)PJvgNvR~ zmdP_6{~970x}L-U|F8MXcKp$Io}-^@_@Q~ZFpq4C1sWuxBVD+{4JkMkKFwAQdMR8JINR3vQfN&Xx@7! zu@aUmnGSOgtz&L?#zoayd>MA9i+dfB7-7qz6(tFWX0J0X`Psd?_{mF_notk zxLh>NHO=(RHKt1Pga$WLZ|4b+U)ivD_3DVS_0Qj>`T71h7|px5n7N1X%me?mvDZdL z&27pdPVIChghKTg-0ajtL>aD3AF7H>hC&|--C@)ko($XTQI&0|kzA7?+p}-eN15>j z%HI$)?@fv8N7OPm_}MFk`LnM`-8nv1lzZTwnrE$-Go{#+_%MCVig<3WOc0097 z4rX=~Z#bHF`qGI7i^L8(`a>1F9GTeq?zxs1o>B=Xx5}WCvI%=;)t_$>#&+QgIU!lL z^j&oiEtiIrt%hZ3>m-|j3M1>x6%=m-nwN}oVLIYXLHEJ=!)lhUpKklr6{eh?+HaUv z9kZ(RTm|EWOHV5GhibRF3BQ>2(jws$sXx<{joc&?ddKZcA^X_ZtTXYn^ zb0MoXVKBNRznA)Lahg<~iZElYjmh2Q$5~@Tdr5-$+?Cd6@;`IcN*$X2R3B`H;*Cc0 zF81}ANV_Cfv;|4ERo1f?4Y~KJ4^qs%-)IoYRj&2-5GJv)F8EYTxh$N<8ZUSGWsIJG zfAy(~CCB1K3*&-L^!vgXG_Sq(iHC_tFNd!f6EZ)Hvuh;3ltn0O6{)UL8Bbtk-Ge*F zNKC6g%7Q83EqyM!?o`4lMChL=wZ}W?bxSS3L;ZJ@zn9Uxkt=E+-myE=IE}r&@Xh)> zbqkSDoJKwgcb>`9jiZH6RYq`}A053J-17Rh^wO%2?$X`ht0P4=x_&x0ES2$D`*l#f zSJ1rtIhNJ5iTGC=A2HkPUUG3QB~9x_H=UjwTL<|TmCc0V7w;LfgS(eCnE?fEUsYx&7#GLzM5`#)%S>{r6;A*v}C3}MF7GYWonl-bg7 z7{wch=8ZGnSKuu!&g=7u%fm|rkLPsV_pY3AO%i;o?jrxI-v*cs50&v<*6)@wN*q0_ zWF}$LPqSJ5jiOv%4dbsuAUuQOjYsq1Tn~S0CFH98kon=m=1EUKJi0Gw9=O($maD`B zrsp=323D2aLar`3I8{E%yDK3?N_n@)%==RPvFzN(3P%^7qrcZkK=Wo5yb3)N$L6DZ zZ*DVkH&>TZZD#FP*0&;cbh2%NR+dczx~oF0i+Z@F4Kj;Vu?t&IoYqyUw0sXrlO!IR zJzblE@;4F9TNoR7pWeUX)fMK|OBEL%WVW7c-0~Q>$>37_Byn*|knGIrJnt)<&rN~j zjpK(lMc=A&* z3GDf@pq5Z)$vI89I3iDdZR%*jHPLLB4h`(*WCG-pK}|NaN2u2{(BH!(p?Q1D{EPR? zX&b$|!g<`;)}H%huUUiHU2XD{`wU0IT_{|A4v!d{B;s86xcTCq@YlfUu;){IGVi{^ zmpd4F)+&*j8s%>?npcquN2Y7z(&8uQG!pZN_)NTZG{bXOx4a~DpYk+{%BCGK#aA2` zvrd&HcX{7Q`Z{h`a?dv{zc)*)!jr{(4bF|N5~2o52ix>1?Kjh!TRIiidsfy#J*#fH-y zr{UIM^zF-``hDfcQM_qr-d;Ld-A}sa)-|!ilXh{*YV)6BZ+Z6mHHbz%-iqVwB~om8 zw;{EGsTFxrmBmN-ChxKj)fG?rSI;da**q?=zD2*6PDk_hvz$4bcT|zk*JvjG$dx|6 zr?`38x_qAYUe-gUU!pL9qfa~UD7?A7(a2G4hDpghFXl|?!;_g1#taAP1@Yf5v)pSCSI4y`&TJq0yE|1r<5>cN& z$(;XmdH3V{CHdp}+OOZ9PI$xi0>}UC^J(eL#p`wY!>t@B-s@=I(Zkm?vjkt_O$afq zMY~$;O^OR3Se51Ze$~6l@uML9B0gbR*vw+I@THVQao>`+Ec3^mvyYDYkwh?9c1rM3 zpx5yYH1Abw``06}W=;me9A91Q4-FAZMP6g-*nQ~Cl@kwX7p&%b#6zr&WfNd1X?r`$2_~v zaWMA4SrqS0G%sWBGUlRGCyD5TM!y9$X48R#Ljxkyc6YunZLYNN`}X^{QZwYXDNY-4-U|0F|zW8hlTM?bD~oG183dyk>NH_Acta*YbIu*+P*U>8QV%xP+}g=f~NcReMu z>=;d;uD(Bswe(dp8pp|*yJN6*SE?1w1DPq#U3i2%zLR_DOkGS2fBw$<&+GMEG;hPH z;GrOXc~A3OGMf?lJWFIBOTOcXPKt83baE!Q)QB4%IY70?bBmdlKK6b*^+H zRch9v=Y*?OZuUU^_ocMPH{?u35%^p)#I1XW!|_q^%|r7}O5Y}CHPsC{;56G%$1}P1 zje8>JdN2Vof9(e;Z-$ZVFE^dWz6Q7*%pD3=-)(!I!0qZ~lGEmv=RO~GofGmuiT=Li zHk#Mp_@1Qw=-81$Z=4Cj!7mmMFKw}P93@zOQ&wUbc#5c&U92)>+{?t^W(9R2hPf#+ zDP{Llyy30=7h7cB+?TrW1?6u(n)ku!h?gH8Ol0v-U*2Fh3r}iu_{yTs$XFzMuEMIy z{e!$M#;oPRAhF;Xy(6D2B+c8!pR#_S7Vv-dSzDpqED{I(-l+i1OOU5f;oj+a$*fDB zgN*-}-Sg9*LxkLv=i6zvNIR-RPG6E9EaZuG)LdJ1D>)EQA~4s@PP0fwuzr$0R=X{D z1wGFS(Y(x${o8k&o`}9Jbb1eVdGb_G-O^~6qb;~lnRJYy8yoR>{E|-}uMIcLtCDWh zN4R^EY?B~6Q8>!x(UY@x5}eWBpB15b6N_oT>(Ayo<>4Pwu5Ro-bxsbCYsE0hI^BUh z^0h=L!KU7m_}5IMRUH-gduXE`2w^+7iYr*2srPvF&E4d<7y5dq7|rXWa*LSV^D3S* zTP<_Qmao}qzXGgYXRKGNG~Wy7L$JI$Xs_*eiMr5(Z-leCc%e%}|Ng`4iI+5nKi0HO zORQZ&zn?Eb^LF)wpTyTAk^6c#|1syc=hy3B+PXZlxK4Xb;fmuuqD!x@`mVEhG%Kly2QREalp0-bj?f*?#AbOV=$81SzH|f?valL6(ewxpA5=l#XO*LQZDi9hRyiNbB_GEe-(Z-G2s4ch zYA`R2z8^(j*iX4PVfb>C_c*V-oyMNRD`V^$KGRuB-V8EnClq4aud8Nm!oQt?Jg2Qd z^FB~wkJzir>g*OJaLI)!Udf@7tZz*9_Tl~0rUxJ8;2v}%6lT43Hv!&DcFw-U|EV;d%)7?>|FQX=*?=Yry)n%MjM8+yG}kBggkKDu_} z#va-|bgHuc&hGZt7fY&UExmfLCfo9za4qn)bPwIUeGkQ3h2{;*EG0etD&VGuT8o#p zs<|S`jcJPymVD*Y67D|41K+YSc?*SHGD=75)8{QiHF(>T3o+9|bn|<62g{CkIoF~0 z$!aw3md`i75so|VUv>-Yco=A8)#txhi|mvrYgiX;4bArwJ?8novrg>E3DfiSC)%vz z7_`7eU@N8PE2i;Sm%G*XO;P^dMf3K)^-oAMnIN;YQA$?|X6P6^%I@73|B}vHvrzK8 z8Bu@v7{-O#e?@Pv8-K3DuKku;y~0Jyo2g6lJyQ#Fm>~51d<~jc>_Uk*S4LV=av@oy zaX`4s$XJKp4N_I6b)u{fR+AKgMU-R_j3x?E|Ttzclj zf%3N&&0DGb_4(nWUOGIV`H2J6>ylUEEAgrfDOuz9;%N~DbJ7X3S$|buE?9Y+!R?tI z+uL$INZldnokWxb_BCAEGe!qdy!X(&Cp}|$sUNqTN?&v)I`x9Wlx|38w)i~P{Hcnj z&u>CMa*#FFYTYJXBHPFQl<1UTC8c|PS6OHfW5E>ry~%K?G?gw9s$)91SM)(kZmsKu_U793Tj526)$Be$jh zB(bdmp67qd?+E;k!0!nBj==xB2;gl~BN}FLK*G2?*g1h0yvn<=u*m=CPxfuM{;hsT z;IAV9=a9WSzqh%g1C|BTkH1$M_}7U4KWkw>%q^YW;I}VpEI$^`(6LP7y&r{+%b+`81SLjrojJy=hYv3ww=e1&okYGf3J`K z#i0k!cz(<82>gz~?+E;k!0!nBj==8-{Eooy2>gz~?+E;k!0!nBj==8-{Eooy2>gz~ z?+E;k!0!nBj==8-{Eooy2>gz~?+E;k!0!nBj==wE@56J@opBs8He}i`Of=Kr@!?!}O@*A5_>XWt`;5X6kw8zN7tkB{ zDS#u;zP*%e!$T{e_#MG5WtaC-?0Gi)s2B3{EZNnQxTrq@O%q+8~6(N8W;m? z0p0+<1oi@-10Mjpflq)vz^A}pfDeI>fL*{2;9X!7uo-w6SP$?n8}DB6cNhi(yl31Q zI2_my+IPS~;0NGbU;%IiFdw)KI1?xX<^q=j6M%`p>A*uM%yy^&T zLwi%+8h}3dbKn8se&9afXTW#>Nr|s?#gqOv4rmRG1&#pdhx7;9cLYG6I}&IE^Z*V8 zh5@|+`YL_69ncHt3A6=T0Y3o_0rCOzF)rmWEv93JQ3s|;Ih5T2pghW9n(cv(0QEf# zU|nH2>kZ@DWs%R+^=Q0PC(*s#1&#p* z0;K@Mj{`_!cy?GZ!b$+f8v+yp#{&Vt51arD0!U{#pG5%aW;kV##&4#@I83h$7!FYH zkpS~Z`>;MR|D%8tfs+81>pj4oz#YH};O78+CI&18$X^SDfnNekfNOwxz}dhrfUAHe z;8K8fauU!8Gyt=KI$#!%0)oH=z3bfH>zPp?O2CfAb1J?o912+Ja zcO$S2_!aPLU^&2i+z#9d{0{gnuoCzUa5Hcdz_MgKhBJ)c#590me7^-)1<=;F;YoYm z3s4@*>>hySayLNPcLAoK+=+KnzDcwDk*N>knEFv(cASX_qg;D9b$JL_4Ll4y3OoX= z1*jKo#xVK>b!XW>4$!xr1fBr)Ka2n1uVI(*Y(Hv7@vsZ)$}q|rmL69WWsb7i!<~OO zu<3-s3$*esc{7Iwh6IMBM-oltYpw`CIP36sZS!Uh2^5tEilqG_Pmfm9`<&6|%1xjY zfd!PKkSJByFOEAEMNn&v#z z8LQ7hfdW<_$H3NlShhD9`Gv9)D}>>2HFkEHD(A_2DWKUD|g-si7`{cmm%PeWB#)Re%>puOWfA+ay|H2a$1=141q^{K^Bgrn$ zJoeRr3vK|V2t`_goTh@QI$hI0?EUP%`L%gpT09G&G0XIkyFa?5^Y91vS?RFVpjQ>< zE&cV(mx83gJ*XSo3sR!b-8OIR&bKEkp5aF8(E*ebHgDVe!0U%k5Q>!P1K=TL)7W=j zetO}}kAOn&g>I~6q)b}z^zdEn7eA<^A)O71QZ_-mwfvAHW(b9qL`GVy3%xJ?ddd75 zJlmtuN(Z>n8t@kT09&}9R|u7@67%#mUx=}rb_!LP!0#>y^~Jd@!sU`?hzib zo3pK*t>aRrxx81jcJNWy`p0PCT(g`#-l0rUat>aFnibVyZ2aB+$aC8see3#D#G>$D zH$QMLQwNVnI-2IM3@7zem+Kp^UVP!_)%kfv0sMoMqv5FL$4}&Mc>eY;zklJ5n3UYm z-0jXhRodZLwU!L0^uuoY?1&@Z-&&QQCzMi*9)2Ptyr%jwf7RAUZU}%;8n=!)HX(owCSNSPga~WssQP*g$2bqKTf?&kuMpn_m{^j z8XHP4nAh~!z$-*+DFx@Y*Ev7Gf^KZz%BSCT`Rd)*mx)(Ni{wxjpmlS*%~N)(izUJd z4fSaHUsg06=f7EbNKPBfQ#NRBH}N-kRe4*Z6b^Je`qA#=R{bqMZwf-XK-&}1DX@Gx z_Sk>^-xhyxsipO`ps>dnIH}*N=f>_hTqqnk_-;gSCr~Cdj=$-tKThaq@UR2A9hAdB znf=-gx14_F);EPBZQ`S#Fuh~;Cl^j^J-xM}K)2^X=?2QD9Yg(-+b{m7qQG|sq4Qz? z^T>_&{B*|~7cHIbib!WIeEakfEcxFL+%lt^^yv2#?(L1j1C`bbdd zqit3$dAa|UyKfbW_-KCw({4Af*xYMY^zjagf^>#}(ixP%s14elUz~leqCnbMMY(a{ zMy;;v*&8frXMoZbJV*X+&WdaAKBrhHQnM}w1>^drejUHO^+4-B%N4~nKhC;2+h7GU zL%&}0Q0t{zi++BMr9sa8h>tqxhMYR5<6LsiT05iz8|3s}sdLU6IQw-66dV0u&b~j5 zxa_{fdHH#899CdoH%0m6zK_q2ty$5Hed7q}8=c##7s12Ym~2(Md(XHV_A84*oI^`4 z6sK|giCqk7hur&OdGY+}8c0J2Oy!*WpVPp@cBjMas>`;$e{e8($_$?RWH6DyNdCE% z+h09)?(BacKgcEjwj5^o^^`?_da&<#r*c++KAV5J#{LGS7Os0F(rI(XochF|FUIHR zsY#CSN>F-&Qn`KXnyLP>k1fhdPzpfV`0}ft^*U=NMgb|4mS{zpnNvvg-vg3cd~pqYeV^H z`m8z))22U_e6y$H+T%1(iUOPhf>M_XM|5Aq@gMY_yZ$+jt5jQ68ID!>>k__ByThG& zJvvzzX;Ru@-OnkiuYP{z?!cLKLqSn~z&R(q*PMldqWZ?}pwK$z+&MpQM4)5@^Rwu1 zgHka2)YZvur>+Oj5I8o*6iY!t)6;a;yvVv853ju+6pR}V~9sz}|OV8HR2J}8|_A-OQfyjDAxp~Earzb7Fi?gXR zW8V0(qQnl2*gvUeN^ej~G2e#PZ-c_N_x!8-T{7a)zdvtC8wTY*R+KR_^Veia;UbxN=zi_Vmk?p+>!>XPeYMhh!5*Dj!-3N>l-KCMqq z|L`v10T(=1QHmpLzux>>HAc5)%^Cs{xwCtbllm<|G-@t{D7rcWMhTr%yW zS>J+E3J(EgDk%83X=lHlUE7cDlrKER9D9cqW#2pfyWeupuCYoQY;dWfTzAWwso!7v z+EkGy9 z2lG2~9$bo50{G}oMHyLi+Bv;0f1lE5QE2^7PzHkM++L?_IqJ)`n=DH9N=di8`54W2CJ&ep-5e-Rhltp#gogXlU*hR!YvfmfVIW3^k?c zv>liCAKm7_L`Z{a0+_vzg3;(9)1UdG)ykcp8)GD){1OzlGSen+opj+>n>a(Jbzrwu zps>zux^`}-Bl5R3urCp_`6Alv7;RC~cHpFmr_cBWc$9}Y=X7r`L)zL+m;G{l)%^n{ z9r2yND@xgo{$>3(bVVu5Ga4kHGtYXMmAbt+=%S|j4=X!EnrcI8wZ_@h?ckoH_kEk{ z&T&;K+Nx5l(_-l?rB(R0ta^XzgTFuOUQo~?21a1j_NPWm5gpXG{N82dXXfW|IRp;k zb8dV8)fSfWvRCG414C`+w)Y$8#&&1t@9*mznsUb~r8Sf@f79r??bmcZaNGT?bEu_M z+u1|%`WQ9%%m=pAfBsfaw!Lgaz!OYXNjIF|`HOAmKD(2vUu?-i>5FQ`w(7oq=iU0< zf*-iI}((kSmQ#i&r#=8yOS-gOWymC0*7j&+c6f}(n7XNp5li?-?k3{NPn{ck>eXMUSq zB}!}9puRdB;#%j+(d)jw?aZ%!E)?;DL@-tDNA-F`f9Bw}DWhl|`VMp}LL0-)aBpv_4~3E#@ky@`JFhHE2lZ?h~fHb9UFy-m-X{Ykh^LCAdDi zz5go%=by579C)A^+uj$CG3>U!(!c_r&prH>@|n`psun5X$H5J(+P~F* zRGS5jSJM;W?&JX_91W*@-&}as;_vm%$Ad!az()@mWTaDg*O>LY7nIYY%qe(smbOqn zQ#@xBK6`7=%^NVnm-YZW-+;n4e!_-O=#^u?PJ)6pYD&Xg(~owsYUz&ZCp(_^?q=Z; z-TrXAQF7(828r81sy#S#u;Dvf&b#;ZiTi%{v8+?b7%69)D1D>(TSMI^etd4n zUk=zM8emf*XInJ_HbBX(R_T%!^sT##wS5xyYp)IQ%<~nU(FuTO~yMf zfWmS7l6NQOt=V$%)z-*+Cn#tdn(}vCHFx=<5{y*M(faqG^aG{qo6mi8(TtIqrZ~5j%{6e&&zR91 zK9%|^E5b>p^~%X*yH;=6bvkT4%ozK(3mA27>cxA1`0j=vW;yUTS@+MTbc0_P%$-{> zvoyyzM0u6-I3!2k$T1Q;#Muv=N5sxEJ*Tl{v2SUi?D97)d2GXm@X{LL)wAEWiZcTVf~NygcC{=K?6*T%l+gE$tOv$yJ%glEU+^J?7ET z@N0Aw@^?EZY?a%^N4@abhq?Y88W+BFwIn$mjXF2m++ z|7<)n4~iPSZD2wJcA#Uy$l0l6I94^LpvXGPP?mX=!I#oQB53_Ty4{gnc0;%FaIBDF zop;_g>WexT{mCgg&yc zFsW6A^;EJk5KBaB^gujWRjAG)6#C6KzZ^vfq^d5_VjQ?*S6y}oMQ9=!Z)o%fD=L!Q zCGyLR2%G<*AU5W!Q_%<$@@vT?jy}X=*ldi2^=hpm6py8}SjtZk2n$B4gRzPTq6K4B zni3-R^K*YN95PM~_;vJb*t}0_Q7wsh70M!zZ3?Ui>xsCIQycM^OBQp)Ms{X2UW}=Xg86XAjl{fQ*0l3Xq$yKRG+B_=-9zG<#S2}W<0}S$P z5eH9(fPoL>1Pe4C8rhmivRMtL*wV|b(hV_r%|^LJddip(T=MkuxRzI1=3Wz+%vb2h zp@n;S+uL>U+CQPGcQrGcV>&7L?Vn@4LQ)hfb4soriC#}*4hY+uL*D~`Jv%>x)Jy@wF{pEF)c(|Cu9u1Q1pHi$|ZN7161DwW-6s4C9k}(sXl3Q(mKmQhwYwO5m9hIf8Lj zMnE)}$oQ_;g0%>9GqpW&6+K%g6)a{Hql9qr2`6^yVii>yHRDiL#|owMfGqC$wa$(J&jgV6jUcY4lfX5rpe5W^>?x&JGl- zj9jEzG*^F*H1eqxYO^JBkUe1tZa0to@k-T_Sd}w4ZMME8X zh)D6$#pWZBVEby34rx~dZ~EP`K?Q0W+D*e5ohz9=8U)!tEmpG(r0-!npV>RKI=pD^QyNQjR@O5h-eljlDssBCDn`)BCHR|!EolfGM=mt zCM(eQCOGd5)VS5sbQ}mvze5i!8+lkToeVhB@8~|gZLiL&gHOGQ?vn4ARuxZ$(ImNb zu~sCAus&L3J>wKT_>32%tn|gW1;&hG7fH6IQgJ`#{+R2#_*`a0$jl7zmzk}xiwrAS z@LL}kd1BMVqXdlUF!6;=yU83HrXPvLwOSLd-FD4QPK&Kjb&Qwm1OcA>Gn zW-O_>7|Rlyna80#;KaO9j-i`-v!T}5*n{%_rR6scPf58YIL2TN4khC{1|NxFeT?T6 zxNOB~bhIvsf6@L{o6B>!_oFE}Sg1;9k$45D0a+s&S6vqk#wzu~7*^KQ${nsNfuS;} zr!Y#v2Cg=%E*O!8uVBOGBPX-NSRRWf zxi6hwHN-TiGF;^kV*RbrP^i8-&ZRyb3xp7vaQ{O5NiKK%b>bAW4~Q07Iy)qij5g9 zheBO!rexW@#Y({*is_j58bb5ZS8f5B>WxjAnwGn$mD}qeCDRSjP4m2qd%I}KN*W6c zNez75m9SbtIJ4ufP_AkwMwiv}jM~?t{xi!9r7DZT z*-FqtUQ0NZg{=0$vb8i991y84$c(LeDBaU&hL4#&nh_u3Gx*VA;?$jo;nMpY;7-3= z>jdS=AbNMSLP5-gT{i}@VnSS22r4T65zoSv(I7?*6@J-af&byi3f|oYR8}Ozii!g^ z^j<#AOpcmmq~D@}NXsj=oEG!2x>?><7Xz)V5AiUshLRhQ;NruIkMq`%I;_@2bXF@Y z+GwF#m&#{Ff+Xuhs}zpTQu`AbM=FM|sVb=;J#vF{+GMDi%_VaF zrh!@Sjc(YVN(OOyK{sY0#z-b>0PdoXOkHv^Ax&qZ^z~jPZiqETRi=`Vnk(4qQM`1u zrdg(fR*3ka*GS1YJPl^!1r^A9|JghU4KDK)7U1rNyZ>g*1i_vag39N;kdZOhgbZWS zDfPC@LxGGrE0f1e5w^z3B^jut$(;APSUw{j#4tL=crVS_C(vNDf5P&(FXWK{H4c)= zxMW@C`0|M!TiH(2lUk?_Re+W;DuYXAtVklzQ7yCd0FEx>L2LYu&KPIqoB=8=Wz8gJ5TNJT+_){r9AP4{#pLxkdxM!>5I*p^k*TL^4>w;UX7BvSLDDR*3Y|Qivu&=BzCcDe-Iq9lDCQx6&Gdri#MH45;lw-!|| zbBJ5R#a|2>tu;g!C9x^VTcFIN@rD*ed5du=0?Ul4YHkH4JQwhp=mbi?wxby^1XsgxJ2M%)HcA z*K@(C-o&%KdVh7@SGeTjFT6@EBe`T%E+d0So+uKvS;0jLEORzS@Pklw39f+ZXv89} zJ4TriAu}@ot)lm?757DXvjyDdYm5BErY~4+U*J|fn5d6T#4w~xxmkqSG~>}S_HQW( zY5t7hSTO452UeuCAabB&ygSAm8yd2}XKrC|c^*=mh1oVyQsG!5J3t&+(Q%|8?04Ei z5g|Dew6C$|AmgmDv1sok7n^-@Hidg8;gFl9a!Ai&MqaZ*Y&mr)&m597^`;RsGV{c@3+qwRO(k%aOI93R?4FJT zA?bIi%w8=SFE4?K4>a>?L(nCq^lB@Z((mZ$aNEdZ_?We@3jVARsqZpG;Sfii(0ASI zH{w8&@q$)wxaSx+hl{G&fw7VaX&K)(H^{*%tB+1K(wJXhw=Y=bdcrP;T&%629zz%7 z#ioQy+2oLup#lxX?MN(Ii%LIQs+DVqv_5{cJTV|d$+Klrve$vw`+9j`+Fy}W`W=Im zB4afq5-u;~uNwG6k#GQ)BU~)aXb>W?<@X^ye4iZyesB^j>dkNb-lPYcQJ}oo0Z#lE zeHpG8k%=GDk+>+Cj-!|*z9m8l=TyQ)h%FYx$h@PN*F&UT!AYoaDD0_-JsO1AKT&0Q z|H;b^Mvas;9|(}8A0DI?zN}5JXgnHs-MP!&c}8@|6gCV!d4H;f+E_au;I}@|bkn8W z=hEg>flDCZmX#0=R9c$0ImIx39<`;3>2fLsXBe&bc7Zdb}=)y*F%&VSUs^SVlZZ%J_zpdhf=Vm%_kozM===wJ_=-cdY&&{^x%I DCk5Tf literal 0 HcmV?d00001 diff --git a/app/server/services/ts/common/port.ts b/app/server/services/ts/common/port.ts new file mode 100644 index 0000000..08c4243 --- /dev/null +++ b/app/server/services/ts/common/port.ts @@ -0,0 +1 @@ +export const PORT = process.env.PORT || 8000 diff --git a/app/server/services/ts/eslint.config.mts b/app/server/services/ts/eslint.config.mts new file mode 100644 index 0000000..cd2f864 --- /dev/null +++ b/app/server/services/ts/eslint.config.mts @@ -0,0 +1,13 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import json from "@eslint/json"; +import css from "@eslint/css"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, + tseslint.configs.recommended, + { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] }, + { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] }, +]); diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json new file mode 100644 index 0000000..f6714c5 --- /dev/null +++ b/app/server/services/ts/package.json @@ -0,0 +1,20 @@ +{ + "devDependencies": { + "@eslint/css": "^0.14.1", + "@eslint/js": "^10.0.1", + "@eslint/json": "^1.0.0", + "@types/bun": "latest", + "eslint": "^9", + "globals": "^17.3.0", + "jiti": "^2.6.1", + "minimatch": "5", + "typescript-eslint": "^8.55.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "cors": "^2.8.6", + "express": "^5.2.1" + } +} diff --git a/app/server/services/ts/tsconfig.json b/app/server/services/ts/tsconfig.json new file mode 100644 index 0000000..28db323 --- /dev/null +++ b/app/server/services/ts/tsconfig.json @@ -0,0 +1,48 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + // "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": [], + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + + "paths": { + "@/*": [ "./common/*" ], + }, + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + } +} diff --git a/app/server/services/user/.gitignore b/app/server/services/ts/user/.gitignore similarity index 100% rename from app/server/services/user/.gitignore rename to app/server/services/ts/user/.gitignore diff --git a/app/server/services/ts/user/Dockerfile b/app/server/services/ts/user/Dockerfile new file mode 100644 index 0000000..75f517d --- /dev/null +++ b/app/server/services/ts/user/Dockerfile @@ -0,0 +1,14 @@ +FROM oven/bun:1.0-slim + +WORKDIR /project +COPY package.json . +COPY tsconfig.json . +COPY common ./common +RUN bun install + +WORKDIR /project/app +COPY user/package.json . +COPY user/src src +RUN bun install + +CMD ["bun", "src/index.ts"] diff --git a/app/server/services/user/README.md b/app/server/services/ts/user/README.md similarity index 100% rename from app/server/services/user/README.md rename to app/server/services/ts/user/README.md diff --git a/app/server/services/ts/user/package.json b/app/server/services/ts/user/package.json new file mode 100644 index 0000000..72fe3cd --- /dev/null +++ b/app/server/services/ts/user/package.json @@ -0,0 +1,9 @@ +{ + "name": "user-service", + "main": "src/index.ts", + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "tsc", + "start": "bun dist/index.js" + } +} diff --git a/app/server/services/user/src/index.ts b/app/server/services/ts/user/src/index.ts similarity index 89% rename from app/server/services/user/src/index.ts rename to app/server/services/ts/user/src/index.ts index 8152996..d7404dd 100644 --- a/app/server/services/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -1,6 +1,6 @@ +import { PORT } from '@/port'; import express from 'express'; const app = express(); -const PORT = process.env.PORT || 8000; app.use(express.json()); diff --git a/app/server/services/user/Dockerfile b/app/server/services/user/Dockerfile deleted file mode 100644 index 8746464..0000000 --- a/app/server/services/user/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM oven/bun:1.0-slim - -WORKDIR /app - -COPY package.json ./ -RUN bun install - -COPY . . - -EXPOSE 3002 - -CMD ["bun", "src/index.ts"] \ No newline at end of file diff --git a/app/server/services/user/bun.lock b/app/server/services/user/bun.lock deleted file mode 100644 index 1090b99..0000000 --- a/app/server/services/user/bun.lock +++ /dev/null @@ -1,179 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "user-s", - "dependencies": { - "express": "^5.2.1", - }, - "devDependencies": { - "@types/bun": "latest", - "@types/express": "^5.0.6", - "@types/node": "^25.1.0", - }, - "peerDependencies": { - "typescript": "^5.9.3", - }, - }, - }, - "packages": { - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], - - "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - } -} diff --git a/app/server/services/user/bun.lockb b/app/server/services/user/bun.lockb deleted file mode 100755 index 57ce3ec74dda46289f1a78a0d3625d5f681ad46a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115888 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 bUAp#xSi)>pD^+piiNeg^Ab3gcfB5Gg-t0)x diff --git a/app/server/services/user/eslint.config.ts b/app/server/services/user/eslint.config.ts deleted file mode 100644 index 673b075..0000000 --- a/app/server/services/user/eslint.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import tseslint from "typescript-eslint"; -import pluginReact from "eslint-plugin-react"; -import { defineConfig } from "eslint/config"; - -export default defineConfig([ - { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } }, - tseslint.configs.recommended, - pluginReact.configs.flat.recommended, -]); diff --git a/app/server/services/user/package.json b/app/server/services/user/package.json deleted file mode 100644 index a236d31..0000000 --- a/app/server/services/user/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "user-s", - "module": "index.ts", - "type": "module", - "private": true, - "devDependencies": { - "@eslint/js": "^9.39.2", - "@types/bun": "latest", - "@types/express": "^5.0.6", - "@types/node": "^25.1.0", - "@typescript-eslint/eslint-plugin": "^8.54.0", - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^9.39.2", - "eslint-plugin-react": "^7.37.5", - "globals": "^17.3.0", - "jiti": "^2.6.1", - "typescript-eslint": "^8.54.0" - }, - "peerDependencies": { - "typescript": "^5.9.3" - }, - "dependencies": { - "express": "^5.2.1" - }, - "scripts": { - "dev": "bun --watch src/index.ts", - "build": "tsc", - "start": "bun dist/index.js" - } -} \ No newline at end of file diff --git a/app/server/services/user/tsconfig.json b/app/server/services/user/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/app/server/services/user/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} From dd12efa38164a26d77c11f742a8a7caae80bf367 Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 11 Feb 2026 13:49:48 -0600 Subject: [PATCH 035/204] fixed/cleaned up git ignore & removed extra READMEs --- app/server/services/{ => python}/.gitignore | 0 app/server/services/ts/.gitignore | 4 +++ app/server/services/ts/api-gateway/.gitignore | 34 ------------------- app/server/services/ts/api-gateway/README.md | 15 -------- app/server/services/ts/auth/.gitignore | 34 ------------------- app/server/services/ts/auth/README.md | 15 -------- app/server/services/ts/user/.gitignore | 34 ------------------- app/server/services/ts/user/README.md | 15 -------- 8 files changed, 4 insertions(+), 147 deletions(-) rename app/server/services/{ => python}/.gitignore (100%) delete mode 100644 app/server/services/ts/api-gateway/.gitignore delete mode 100644 app/server/services/ts/api-gateway/README.md delete mode 100644 app/server/services/ts/auth/.gitignore delete mode 100644 app/server/services/ts/auth/README.md delete mode 100644 app/server/services/ts/user/.gitignore delete mode 100644 app/server/services/ts/user/README.md diff --git a/app/server/services/.gitignore b/app/server/services/python/.gitignore similarity index 100% rename from app/server/services/.gitignore rename to app/server/services/python/.gitignore diff --git a/app/server/services/ts/.gitignore b/app/server/services/ts/.gitignore index 9b1ee42..952c01d 100644 --- a/app/server/services/ts/.gitignore +++ b/app/server/services/ts/.gitignore @@ -14,6 +14,10 @@ lerna-debug.log* .cache +# Bun stuff +**/bun.lockb +**/node_modules + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json diff --git a/app/server/services/ts/api-gateway/.gitignore b/app/server/services/ts/api-gateway/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/app/server/services/ts/api-gateway/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/app/server/services/ts/api-gateway/README.md b/app/server/services/ts/api-gateway/README.md deleted file mode 100644 index 818da4b..0000000 --- a/app/server/services/ts/api-gateway/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# api-gateway-s - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/services/ts/auth/.gitignore b/app/server/services/ts/auth/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/app/server/services/ts/auth/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/app/server/services/ts/auth/README.md b/app/server/services/ts/auth/README.md deleted file mode 100644 index e0b08cd..0000000 --- a/app/server/services/ts/auth/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# auth-s - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/services/ts/user/.gitignore b/app/server/services/ts/user/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/app/server/services/ts/user/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/app/server/services/ts/user/README.md b/app/server/services/ts/user/README.md deleted file mode 100644 index cca60c8..0000000 --- a/app/server/services/ts/user/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# user-s - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. From 695ebbf8552cb32a006634f0e8524e94cac18e0e Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 11 Feb 2026 13:50:50 -0600 Subject: [PATCH 036/204] removed bun.locb --- app/server/services/ts/auth/bun.lockb | Bin 6418 -> 0 bytes app/server/services/ts/bun.lockb | Bin 72207 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100755 app/server/services/ts/auth/bun.lockb delete mode 100755 app/server/services/ts/bun.lockb diff --git a/app/server/services/ts/auth/bun.lockb b/app/server/services/ts/auth/bun.lockb deleted file mode 100755 index c9d0fb033da37250057cce6ddf83c94391808f4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6418 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 diff --git a/app/server/services/ts/bun.lockb b/app/server/services/ts/bun.lockb deleted file mode 100755 index a760a39cb2ef01e1be48b9e7250a248a0a45ab10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72207 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 DCk5Tf From ca0543f03c9f97331b885fc17a2ec7e0c31823ab Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 11 Feb 2026 14:10:17 -0600 Subject: [PATCH 037/204] added readme --- app/server/services/ts/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 app/server/services/ts/README.md diff --git a/app/server/services/ts/README.md b/app/server/services/ts/README.md new file mode 100644 index 0000000..50a0879 --- /dev/null +++ b/app/server/services/ts/README.md @@ -0,0 +1,10 @@ +### Typescript services +Each service inherits from this directory's: +* packages +* tsconfig +* eslint.config + +Unless a package will be used in multiple modules install it only in the subdirectory (by running `bun install` in the module directory, not this one). + +The `common` folder is home for modules that are used by one or more services. For example `common/port.ts` is used by all services to determine which port to bind their webserver to. + From b488c9d2f208ef30428508e0031ad01e3981b10b Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 11 Feb 2026 14:16:26 -0600 Subject: [PATCH 038/204] moved db folder to services --- app/server/{ => services}/database/schema.sql | 0 app/server/{ => services}/database/updateSchema.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/server/{ => services}/database/schema.sql (100%) rename app/server/{ => services}/database/updateSchema.sh (100%) diff --git a/app/server/database/schema.sql b/app/server/services/database/schema.sql similarity index 100% rename from app/server/database/schema.sql rename to app/server/services/database/schema.sql diff --git a/app/server/database/updateSchema.sh b/app/server/services/database/updateSchema.sh similarity index 100% rename from app/server/database/updateSchema.sh rename to app/server/services/database/updateSchema.sh From ecaa139b17018379423689c4f61fcc16604a49d0 Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 11 Feb 2026 14:40:49 -0600 Subject: [PATCH 039/204] factored out some common methods --- app/server/services/ts/user/src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index d7404dd..a783fd1 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -1,13 +1,16 @@ import { PORT } from '@/port'; +import { onExit } from '@/hooks'; +import { buildCorsConfig } from '@/corsUtil'; import express from 'express'; -const app = express(); +const app = express(); app.use(express.json()); +app.use(buildCorsConfig()); const server = app.listen(PORT, () => { console.log(`User Service running on port ${PORT}`); }); -process.on("SIGTERM", () => server.close()); +onExit(() => server.close()); //test endpoint app.get('/health', (req: express.Request, res: express.Response) => { From 61ae0a0c02f3fd747b23c7e0a6110541e7c820b1 Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 11 Feb 2026 14:41:16 -0600 Subject: [PATCH 040/204] factored out some common methods --- .../services/ts/api-gateway/src/index.ts | 27 +++++++------------ app/server/services/ts/common/corsUtil.ts | 12 +++++++++ app/server/services/ts/common/hooks.ts | 3 +++ 3 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 app/server/services/ts/common/corsUtil.ts create mode 100644 app/server/services/ts/common/hooks.ts diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 7341409..8a92665 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -1,3 +1,10 @@ +import { PORT } from '@/port'; +import { buildCorsConfig } from '@/corsUtil'; +import { onExit } from '@/hooks'; +import express from 'express'; +// import { createProxyMiddleware } from 'http-proxy-middleware'; + + /* This service should have express, cors and http-proxy-middleware installed as dependencies. It will act as an API Gateway, routing requests to the appropriate microservices. In the real world, the other @@ -7,36 +14,22 @@ The /api/test endpoint will try to reach every other stood up service and return */ -import { PORT } from '@/port'; -import express from 'express'; -// import { createProxyMiddleware } from 'http-proxy-middleware'; -import cors from 'cors'; - - const app = express(); - -app.use(cors({ - origin: 'http://localhost:8080', // vite dev server so that the client can access this API - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] -})); - +app.use(buildCorsConfig()); app.use(express.json()); + const server = app.listen(PORT, () => { console.log(`API Gateway running on port ${PORT}`); }); -process.on("SIGTERM", () => server.close()); +onExit(() => server.close()); //test endpoint app.get('/health', async (req: express.Request, res: express.Response) => { const result: { [string]: string} = {}; const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); - console.log(services); for(const service of services) { - console.log(process.env[service]); const serviceName = service.split('_')[0]; result[serviceName] = await fetch(`${process.env[service]}/health`) .then((res) => res.text()) diff --git a/app/server/services/ts/common/corsUtil.ts b/app/server/services/ts/common/corsUtil.ts new file mode 100644 index 0000000..1b901df --- /dev/null +++ b/app/server/services/ts/common/corsUtil.ts @@ -0,0 +1,12 @@ +import cors from 'cors'; + +const defaultMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']; +const defaultOrigin = 'http://localhost:8080'; +export function buildCorsConfig(opts: { origin?: string, methods?: string[] } | undefined) { + return cors({ + origin: opts?.origin ?? defaultOrigin, + methods: opts?.methods ?? defaultMethods, + credentials: true, + allowedHeaders: ['Content-Type', 'Authorization'] + }); +} diff --git a/app/server/services/ts/common/hooks.ts b/app/server/services/ts/common/hooks.ts new file mode 100644 index 0000000..0c6083d --- /dev/null +++ b/app/server/services/ts/common/hooks.ts @@ -0,0 +1,3 @@ +export function onExit(callback: () => void) { + process.on("SIGTERM", callback); +} From 7be455edaf4d406ff17337e37864d2101829a4c0 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Thu, 12 Feb 2026 11:12:48 -0600 Subject: [PATCH 041/204] Client: Polished transaction table and added method to fetch transactions --- app/client/bun.lock | 58 ++++++++++++++ app/client/package.json | 1 + app/client/src/api/TransactionService.tsx | 18 +++++ .../src/components/TransactionTable.tsx | 78 ++++++++++++++----- app/client/src/utils/constants.ts | 1 + 5 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 app/client/src/utils/constants.ts diff --git a/app/client/bun.lock b/app/client/bun.lock index 90eb525..9f8aefd 100644 --- a/app/client/bun.lock +++ b/app/client/bun.lock @@ -7,12 +7,16 @@ "dependencies": { "@tailwindcss/vite": "^4.1.18", "@types/react-router-dom": "^5.3.3", + "axios": "^1.13.5", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", "radix-ui": "^1.4.3", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.13.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", @@ -120,6 +124,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@oxc-project/runtime": ["@oxc-project/runtime@0.97.0", "", {}, "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w=="], @@ -384,6 +390,10 @@ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], @@ -392,12 +402,16 @@ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -406,6 +420,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -420,14 +436,26 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="], "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -468,20 +496,38 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], @@ -552,6 +598,12 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -582,14 +634,20 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="], + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], diff --git a/app/client/package.json b/app/client/package.json index 44980a1..f182384 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.18", "@types/react-router-dom": "^5.3.3", + "axios": "^1.13.5", "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/app/client/src/api/TransactionService.tsx b/app/client/src/api/TransactionService.tsx index e69de29..661e654 100644 --- a/app/client/src/api/TransactionService.tsx +++ b/app/client/src/api/TransactionService.tsx @@ -0,0 +1,18 @@ +import axios from "axios"; +import { BASE_URL } from "@/utils/constants"; +import type { Transaction } from "@/types/Transaction"; +async function getTransactions(): Promise { + try { + const response = await axios.get(`${BASE_URL}/api/transactions`); + if (response.status !== 200) { + throw new Error(`Failed to fetch transactions: ${response.statusText}`); + } + return response.data; + } catch (error) { + console.error("Error fetching transactions:", error); + throw error; + } +} +export default { + getTransactions, +}; \ No newline at end of file diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index 806ab86..abf8b46 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -95,29 +95,65 @@ const transactions: Transaction[] = [ ]; function TransactionTable() { return ( -

- -
Date
-
Description
-
Category
-
Amount
-
From
-
To
-
- {transactions.map((tx) => ( - -
{new Date(tx.date).toLocaleDateString()}
-
{tx.description || "N/A"}
-
{tx.category}
-
- {tx.amount < 0 ? `-$${Math.abs(tx.amount).toFixed(2)}` : `$${tx.amount.toFixed(2)}`} + + ) +} +function Grid() { + return ( +
+ {/* Header */} +
+
Date
+
Description
+
Category
+
Amount
+
From
+
To
+
+ + {/* Body */} +
+ {transactions.map((tx, index) => ( +
+
+ {new Date(tx.date).toLocaleDateString()} +
+ +
+ {tx.description || "N/A"} +
+ +
+ {tx.category} +
+ +
+ {tx.amount < 0 + ? `-$${Math.abs(tx.amount).toFixed(2)}` + : `$${tx.amount.toFixed(2)}`} +
+ +
+ {tx.from} +
+ +
+ {tx.to} +
-
{tx.from}
-
{tx.to}
- - ))} + ))} +
+ ) } - export default TransactionTable \ No newline at end of file diff --git a/app/client/src/utils/constants.ts b/app/client/src/utils/constants.ts new file mode 100644 index 0000000..b95b2f7 --- /dev/null +++ b/app/client/src/utils/constants.ts @@ -0,0 +1 @@ +export const BASE_URL = 'https://api.example.com'; \ No newline at end of file From a11b29df7c122d7cb80bb0753246f2a8ed33efeb Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 12 Feb 2026 20:35:18 -0600 Subject: [PATCH 042/204] created bun init files for user input models --- app/server/services/database/.gitignore | 34 ++++ app/server/services/database/Dockerfile | 12 ++ app/server/services/database/README.md | 15 ++ app/server/services/database/bun.lock | 190 +++++++++++++++++++++ app/server/services/database/package.json | 20 +++ app/server/services/database/tsconfig.json | 29 ++++ 6 files changed, 300 insertions(+) create mode 100644 app/server/services/database/.gitignore create mode 100644 app/server/services/database/Dockerfile create mode 100644 app/server/services/database/README.md create mode 100644 app/server/services/database/bun.lock create mode 100644 app/server/services/database/package.json create mode 100644 app/server/services/database/tsconfig.json diff --git a/app/server/services/database/.gitignore b/app/server/services/database/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/app/server/services/database/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/app/server/services/database/Dockerfile b/app/server/services/database/Dockerfile new file mode 100644 index 0000000..0807b34 --- /dev/null +++ b/app/server/services/database/Dockerfile @@ -0,0 +1,12 @@ +FROM oven/bun:1.0-slim + +WORKDIR /app + +COPY package.json bun.lock ./ +RUN bun install + +COPY . . + +EXPOSE 3000 + +CMD ["bun", "run", "src/index.ts"] diff --git a/app/server/services/database/README.md b/app/server/services/database/README.md new file mode 100644 index 0000000..1d3c175 --- /dev/null +++ b/app/server/services/database/README.md @@ -0,0 +1,15 @@ +# database-service + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/services/database/bun.lock b/app/server/services/database/bun.lock new file mode 100644 index 0000000..afb1868 --- /dev/null +++ b/app/server/services/database/bun.lock @@ -0,0 +1,190 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "database-service", + "dependencies": { + "express": "^4.18.2", + "mysql2": "^3.6.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + + "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "mysql2": ["mysql2@3.17.1", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.3", "named-placeholders": "^1.1.6", "seq-queue": "^0.0.5", "sql-escaper": "^1.3.2" } }, "sha512-UzIzdVwPXPoZm+FaJ4lNsRt28HtUwt68gpLH7NP1oSjd91M5Qn1XJzbIsSRMRc5CV3pvktLNshmbaFfMYqPBhQ=="], + + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "sql-escaper": ["sql-escaper@1.3.2", "", {}, "sha512-lp+ZDVfSjHt+qAK1jXBTIXBNYnbo7gnaAGwoYTH9bE89kNkXwcu6g0WjJGRsdTKVpY1z70u3Y0IgmnBOoRybHw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + } +} diff --git a/app/server/services/database/package.json b/app/server/services/database/package.json new file mode 100644 index 0000000..91e07b7 --- /dev/null +++ b/app/server/services/database/package.json @@ -0,0 +1,20 @@ +{ + "name": "database-service", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "bun run src/index.ts" + }, + "dependencies": { + "express": "^4.18.2", + "mysql2": "^3.6.0" + }, + "module": "index.ts", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/app/server/services/database/tsconfig.json b/app/server/services/database/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/app/server/services/database/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From 99200fa9b0bb2005fd1090312a1daa1aac886ce5 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 12 Feb 2026 20:36:30 -0600 Subject: [PATCH 043/204] Moved index.ts out of routes to be under src --- app/server/services/database/src/index.ts | 17 +++++++++++++++++ .../services/database/src/routes/index.ts | 13 ------------- 2 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 app/server/services/database/src/index.ts delete mode 100644 app/server/services/database/src/routes/index.ts diff --git a/app/server/services/database/src/index.ts b/app/server/services/database/src/index.ts new file mode 100644 index 0000000..ab8dacb --- /dev/null +++ b/app/server/services/database/src/index.ts @@ -0,0 +1,17 @@ +//Index file for database service, sets up express server and routes + +import express from "express"; +import { accountsRouter } from "./routes/account"; +import { transactionsRouter } from "./routes/transaction"; +import { profilesRouter } from "./routes/profile"; + +const app = express(); +app.use(express.json()); + +app.use("/accounts", accountsRouter); +app.use("/transactions", transactionsRouter); +app.use("/profiles", profilesRouter); + +app.listen(3000, () => { + console.log("Database service running on port 3000"); +}); diff --git a/app/server/services/database/src/routes/index.ts b/app/server/services/database/src/routes/index.ts deleted file mode 100644 index 915ce6b..0000000 --- a/app/server/services/database/src/routes/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -//Index file for database service, sets up express server and routes - -import express from "express"; -import { accountsRouter } from "./account"; -import { transactionsRouter } from "./transaction"; -import { profilesRouter } from "./profile"; - -const app = express(); -app.use(express.json()); - -app.use("/accounts", accountsRouter); -app.use("/transactions", transactionsRouter); -app.use("/profiles", profilesRouter); \ No newline at end of file From 4c183aedb57b06cbfa316fd480d8280c9d5659cf Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 12 Feb 2026 20:37:19 -0600 Subject: [PATCH 044/204] Added Api routes for user input models in api gateways --- app/server/services/api-gateway/src/index.ts | 30 +++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/app/server/services/api-gateway/src/index.ts b/app/server/services/api-gateway/src/index.ts index f5676c8..3c2d3f8 100644 --- a/app/server/services/api-gateway/src/index.ts +++ b/app/server/services/api-gateway/src/index.ts @@ -8,7 +8,7 @@ The /api/test endpoint will try to reach every other stood up service and return */ import express from 'express'; -// import { createProxyMiddleware } from 'http-proxy-middleware'; +import { createProxyMiddleware } from 'http-proxy-middleware'; import cors from 'cors'; @@ -44,3 +44,31 @@ process.on("SIGTERM", () => server.close()); } res.json(result); }); + +// Api Gateway routes to account, transaction and profiles +app.use( + "/api/accounts", + createProxyMiddleware({ + target: process.env.DATABASE_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { "^/api/accounts": "/accounts" } + }) +); + +app.use( + "/api/transactions", + createProxyMiddleware({ + target: process.env.DATABASE_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { "^/api/transactions": "/transactions" } + }) +); + +app.use( + "/api/profiles", + createProxyMiddleware({ + target: process.env.DATABASE_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { "^/api/profiles": "/profiles" } + }) +); From 5d9abc27e736ef4a42a29f8a51ac0a60a1fa3183 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 13 Feb 2026 09:42:40 -0600 Subject: [PATCH 045/204] Server: cleaned up post merge moving dev into visualization, health test works now --- .../services/ts/api-gateway/src/index.ts | 2 + app/server/services/ts/auth/src/index.ts | 225 ++++++++++-------- 2 files changed, 124 insertions(+), 103 deletions(-) diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 8a92665..6c82fbf 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -34,6 +34,8 @@ onExit(() => server.close()); result[serviceName] = await fetch(`${process.env[service]}/health`) .then((res) => res.text()) .catch((err) => err.message); + console.log("received response"); } + res.json(result); }); diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts index a27de80..0a7e429 100644 --- a/app/server/services/ts/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -1,118 +1,137 @@ -import { stmts } from "./db"; - - -function json(body: unknown, status = 200) { - return new Response(JSON.stringify(body), { - status, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "http://localhost:5173", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - }); -} - - -function badRequest(message: string) { - return json({ ok: false, error: message }, 400); -} - - -function getOptions() { - return { - status: 204, - headers: { - "Access-Control-Allow-Origin": "http://localhost.*", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - } -} - -// ---- server ---- -Bun.serve({ - port: PORT, - async fetch(req) { - const url = new URL(req.url); - - if (req.method === "OPTIONS") { - return new Response(null, getOptions()) - }; - - if(url.pathname === '/api/auth/token' && req.method === "POST") { - let body; - try { - body = await req.json(); - if(!body.email || !body.password) { throw new Error() } - } catch { - return badRequest("Body must be JSON containing username and password."); - } - - if(!validateEmail(body.email)) { - return - } - if(!validatePassword(body.password)) { - - } - - - - } - - // ---- SIGNUP ---- - if (req.method === "POST" && url.pathname === "/api/auth/signup") { +import { PORT } from '@/port'; +import { onExit } from '@/hooks'; +import { buildCorsConfig } from '@/corsUtil'; +import express from 'express'; + +const app = express(); +app.use(express.json()); +app.use(buildCorsConfig()); + +const server = app.listen(PORT, () => { + console.log(`User Service running on port ${PORT}`); +}); +onExit(() => server.close()); + +//test endpoint +app.get('/health', (req: express.Request, res: express.Response) => { + res.send('ok'); +}); + +// import { stmts } from "./db"; + + +// function json(body: unknown, status = 200) { +// return new Response(JSON.stringify(body), { +// status, +// headers: { +// "Content-Type": "application/json", +// "Access-Control-Allow-Origin": "http://localhost:5173", +// "Access-Control-Allow-Headers": "Content-Type", +// "Access-Control-Allow-Methods": "GET,POST,OPTIONS", +// }, +// }); +// } + + +// function badRequest(message: string) { +// return json({ ok: false, error: message }, 400); +// } + + +// function getOptions() { +// return { +// status: 204, +// headers: { +// "Access-Control-Allow-Origin": "http://localhost.*", +// "Access-Control-Allow-Headers": "Content-Type", +// "Access-Control-Allow-Methods": "GET,POST,OPTIONS", +// }, +// } +// } + +// // ---- server ---- +// Bun.serve({ +// port: PORT, +// async fetch(req) { +// const url = new URL(req.url); + +// if (req.method === "OPTIONS") { +// return new Response(null, getOptions()) +// }; + +// if(url.pathname === '/api/auth/token' && req.method === "POST") { +// let body; +// try { +// body = await req.json(); +// if(!body.email || !body.password) { throw new Error() } +// } catch { +// return badRequest("Body must be JSON containing username and password."); +// } + +// if(!validateEmail(body.email)) { +// return +// } +// if(!validatePassword(body.password)) { + +// } + + + +// } + +// // ---- SIGNUP ---- +// if (req.method === "POST" && url.pathname === "/api/auth/signup") { - const email = body.email.trim().toLowerCase(); - const password = body.password; +// const email = body.email.trim().toLowerCase(); +// const password = body.password; - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); - if (password.length < 8) return badRequest("Password must be at least 8 characters."); +// if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); +// if (password.length < 8) return badRequest("Password must be at least 8 characters."); - const existing = stmts.findByEmail.get(email) as any; - if (existing) return json({ ok: false, error: "Email already exists." }, 409); +// const existing = stmts.findByEmail.get(email) as any; +// if (existing) return json({ ok: false, error: "Email already exists." }, 409); - const passwordHash = await hashPassword(password); - stmts.createUser.run(email, passwordHash); +// const passwordHash = await hashPassword(password); +// stmts.createUser.run(email, passwordHash); - return json({ ok: true, message: "Account created." }, 201); - } +// return json({ ok: true, message: "Account created." }, 201); +// } - // ---- LOGIN ---- - if (req.method === "POST" && url.pathname === "/api/auth/login") { - const body = (await req.json().catch(() => null)) as null | { - email?: string; - password?: string; - }; +// // ---- LOGIN ---- +// if (req.method === "POST" && url.pathname === "/api/auth/login") { +// const body = (await req.json().catch(() => null)) as null | { +// email?: string; +// password?: string; +// }; - if (!body?.email || !body?.password) { - return badRequest("Email and password are required."); - } +// if (!body?.email || !body?.password) { +// return badRequest("Email and password are required."); +// } - const email = body.email.trim().toLowerCase(); - const password = body.password; +// const email = body.email.trim().toLowerCase(); +// const password = body.password; - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); +// if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); - const user = stmts.findByEmail.get(email) as any; - if (!user) return json({ ok: false, error: "Invalid email or password." }, 401); +// const user = stmts.findByEmail.get(email) as any; +// if (!user) return json({ ok: false, error: "Invalid email or password." }, 401); - const valid = await verifyPassword(password, user.password_hash); - if (!valid) return json({ ok: false, error: "Invalid email or password." }, 401); +// const valid = await verifyPassword(password, user.password_hash); +// if (!valid) return json({ ok: false, error: "Invalid email or password." }, 401); - return json({ - ok: true, - message: "Logged in!", - user: { id: user.id, email: user.email }, - }); - } +// return json({ +// ok: true, +// message: "Logged in!", +// user: { id: user.id, email: user.email }, +// }); +// } - if (req.method === "GET" && url.pathname === "/api/health") { - return json({ ok: true, status: "up" }); - } +// if (req.method === "GET" && url.pathname === "/api/health") { +// return json({ ok: true, status: "up" }); +// } - return json({ ok: false, error: "Not found" }, 404); - }, -}); +// return json({ ok: false, error: "Not found" }, 404); +// }, +// }); -console.log(`Auth service running on ${PORT}`); +// console.log(`Auth service running on ${PORT}`); From f83ed8d38d946c57377cb3df57caa45f63256417 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 13 Feb 2026 10:24:28 -0600 Subject: [PATCH 046/204] DB: expanded account types in schema --- app/server/services/database/schema.sql | 8 ++++++-- app/server/services/ts/api-gateway/src/index.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql index d4d58d1..274e7bb 100644 --- a/app/server/services/database/schema.sql +++ b/app/server/services/database/schema.sql @@ -52,12 +52,16 @@ CREATE TABLE finus.profile_goal ( ); CREATE TABLE finus.financialAccountType ( - type VARCHAR(50) NOT NULL, + -- type VARCHAR(50) NOT NULL, + -- Note that there are other types of accounts such as joint, business, transmission, etc. This simplified set is good enough for now + type ENUM('savings', 'chequing', 'credit') DEFAULT 'unconfirmed', PRIMARY KEY (type) ); CREATE TABLE finus.financialAccountSubtype( - type VARCHAR(50) NOT NULL, + -- type VARCHAR(50) NOT NULL, + -- These subtypes are really only needed for savings accounts as they can be taxed differently and might have weird rules about them + type ENUM('RRSP', 'TFSA', 'FHSA', 'RESP', 'RDSP') DEFAULT 'NA', PRIMARY KEY (type) ); diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 6c82fbf..6b8c70f 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -24,7 +24,7 @@ const server = app.listen(PORT, () => { onExit(() => server.close()); -//test endpoint +//API gateway sits on port 3000 and is accessible from there. Go to browser and type http://localhost:3000/health and you should see which services are up. app.get('/health', async (req: express.Request, res: express.Response) => { const result: { [string]: string} = {}; const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); From 85b4dbabc296a7df956401481e0b2e97ee6fc2ce Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Mon, 16 Feb 2026 05:14:03 -0600 Subject: [PATCH 047/204] Created the popup forms components for account and transactions. --- app/client/package-lock.json | 2209 +++++++++++++++++ app/client/src/App.tsx | 12 + app/client/src/components/ui/AccountForm.tsx | 98 + .../src/components/ui/TransationForm.tsx | 81 + 4 files changed, 2400 insertions(+) create mode 100644 app/client/package-lock.json create mode 100644 app/client/src/components/ui/AccountForm.tsx create mode 100644 app/client/src/components/ui/TransationForm.tsx diff --git a/app/client/package-lock.json b/app/client/package-lock.json new file mode 100644 index 0000000..1bc1103 --- /dev/null +++ b/app/client/package-lock.json @@ -0,0 +1,2209 @@ +{ + "name": "finus-client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "finus-client", + "version": "0.0.0", + "dependencies": { + "@types/react-router-dom": "^5.3.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "npm:rolldown-vite@7.2.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.97.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.97.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.50", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.15.11", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.11", + "@swc/core-darwin-x64": "1.15.11", + "@swc/core-linux-arm-gnueabihf": "1.15.11", + "@swc/core-linux-arm64-gnu": "1.15.11", + "@swc/core-linux-arm64-musl": "1.15.11", + "@swc/core-linux-x64-gnu": "1.15.11", + "@swc/core-linux-x64-musl": "1.15.11", + "@swc/core-win32-arm64-msvc": "1.15.11", + "@swc/core-win32-ia32-msvc": "1.15.11", + "@swc/core-win32-x64-msvc": "1.15.11" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2", + "@swc/core": "^1.15.11" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "dev": true, + "license": "ISC" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.50", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.97.0", + "@rolldown/pluginutils": "1.0.0-beta.50" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.50", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", + "@rolldown/binding-darwin-x64": "1.0.0-beta.50", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.50", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "name": "rolldown-vite", + "version": "7.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.97.0", + "fdir": "^6.5.0", + "lightningcss": "^1.30.2", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-beta.50", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.25.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 522aa2f..ed9a0cf 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -6,6 +6,8 @@ import { Suspense, useState, lazy} from 'react' import './App.css' import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom' import LoadingSpinner from './components/ui/LoadingSpinner.tsx' +import AccountPopup from './components/ui/AccountForm.tsx' +import TransactionPopup from './components/ui/TransationForm.tsx' function App() { return ( @@ -42,6 +44,12 @@ function NotFoundPage() { const [count, setCount] = useState(0) const [response, setResponse] = useState('') + //Testing popups + const [seen, setSeen] = useState(false) + function togglePopup(){ + setSeen(!seen) + }; + return ( <>

Vite + React

@@ -60,6 +68,10 @@ function NotFoundPage() {

Click on the Vite and React logos to learn more

+ + + {seen ? ():null} + {seen ? ():null} ) } diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx new file mode 100644 index 0000000..a6e6498 --- /dev/null +++ b/app/client/src/components/ui/AccountForm.tsx @@ -0,0 +1,98 @@ +import React, { useState } from "react"; + +interface popupProp{ + toggle: () => void; + edit: boolean +} + +const accountCategory = { + SAVING:"Saving", + CHEQUING: "Chequing", + INVESTMENT: "Investment", + DEBT: "Debt" +} + +export default function popupForm({toggle, edit}:popupProp,){ + + //Add whne getUserAccounts is implemented + //const userAccounts = getUserAccounts() + + const [formInput, setFormInput] = useState({name: '', balence: 0, type:'', subType:'', }) + const [file, setFile] = useState({statement: null}) + + //Submits the account data + const handleSubmit = () => { + toggle() + } + + + const handleChange = (event:React.ChangeEvent) => { + setFormInput({...formInput, [event.target.name] : event.target.value}); + } + + //Handles the bank statement + const handleFile = (event:React.ChangeEvent) => { + + } + + return( + <> +
+ + {edit ? (

Edit Account

):(

Create Account

)} + + handleSubmit()}> + + + +

+ + + +

+ + + +

+ + + +

+ + {(formInput.type == "debt" || formInput.type == "saving") ? ( + <> + + +

+ + ): null + } + + {(formInput.type == "saving") ? ( + <> + + +

+ + ):null + } + + + + +
+ + ) +} + diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx new file mode 100644 index 0000000..cd1b8c8 --- /dev/null +++ b/app/client/src/components/ui/TransationForm.tsx @@ -0,0 +1,81 @@ +import { useState } from "react"; + +interface popupProp{ + toggle: () => void; + edit: boolean +} + + +const transferCategory = { + FOOD: 'Food', + HOUSING: "Housing", + UTIL: "Utilzities", + TRANSPORTATION: "Transportation", + OTHER: "Other", +}; + +//Gets the keys of the enum +type typeOfTransfers = keyof typeof transferCategory + +//Returns a form of for the user to enter their info +export default function popupForm({toggle, edit}:popupProp){ + + //Handles the submiting the form + const handleSubmit = () =>{ + toggle() + //Determine if the input is valid + if(selectedType && amount != 0) { + //Insert function that handles api communication between client and server + } + } + + const handleFile = () =>{ + + } + + //Holds state of user input + const [selectedType, setSelectedType] = useState(undefined) + const [amount, setAmount] = useState(0) + const [file, setFile] = useState(null) + + //Holds the types of transfers + const transCat: typeOfTransfers [] = Object.keys(transferCategory) as typeOfTransfers[]; + + return( + <> +
+ {edit ? (

Edit Transaction

):(

Create Transaction

)} +
+ + + + +

+ + + +

+ + + setAmount(Number(event.target.value))}/> +

+ + + +

+ + + + {edit ?():()} +
+
+ + ) +} + + \ No newline at end of file From 93d487962b261ca75f034ec325033134f7bf7b21 Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 16 Feb 2026 09:40:58 -0600 Subject: [PATCH 048/204] added prettier to format code --- app/server/services/ts/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json index f6714c5..896ae1a 100644 --- a/app/server/services/ts/package.json +++ b/app/server/services/ts/package.json @@ -1,4 +1,7 @@ { + "scripts": { + "format": "bun prettier --write \"**/src/*.ts|**/*.ts\"" + }, "devDependencies": { "@eslint/css": "^0.14.1", "@eslint/js": "^10.0.1", @@ -8,6 +11,7 @@ "globals": "^17.3.0", "jiti": "^2.6.1", "minimatch": "5", + "prettier": "3.8.1", "typescript-eslint": "^8.55.0" }, "peerDependencies": { From ba7b366ac31bd7a4b7d1916c7e85313ade27af8d Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 16 Feb 2026 10:21:46 -0600 Subject: [PATCH 049/204] formatted server code --- .../services/ts/api-gateway/src/index.ts | 36 +++++------ app/server/services/ts/auth/src/index.ts | 62 ++++++++++--------- app/server/services/ts/common/corsUtil.ts | 16 ++--- app/server/services/ts/common/hooks.ts | 2 +- app/server/services/ts/common/port.ts | 2 +- app/server/services/ts/package.json | 4 +- app/server/services/ts/user/src/index.ts | 12 ++-- 7 files changed, 70 insertions(+), 64 deletions(-) diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 8a92665..9c8ee0d 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -1,10 +1,9 @@ -import { PORT } from '@/port'; -import { buildCorsConfig } from '@/corsUtil'; -import { onExit } from '@/hooks'; -import express from 'express'; +import { PORT } from "@/port"; +import { buildCorsConfig } from "@/corsUtil"; +import { onExit } from "@/hooks"; +import express from "express"; // import { createProxyMiddleware } from 'http-proxy-middleware'; - /* This service should have express, cors and http-proxy-middleware installed as dependencies. It will act as an API Gateway, routing requests to the appropriate microservices. In the real world, the other @@ -23,17 +22,18 @@ const server = app.listen(PORT, () => { }); onExit(() => server.close()); - //test endpoint - app.get('/health', async (req: express.Request, res: express.Response) => { - const result: { [string]: string} = {}; - const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); - - for(const service of services) { - const serviceName = service.split('_')[0]; - result[serviceName] = await fetch(`${process.env[service]}/health`) - .then((res) => res.text()) - .catch((err) => err.message); - } - res.json(result); - }); +app.get("/health", async (req: express.Request, res: express.Response) => { + const result: { [string]: string } = {}; + const services: string[] = Object.keys(process.env).filter((x) => + /^.*_SERVICE_ADDR$/.test(x), + ); + + for (const service of services) { + const serviceName = service.split("_")[0]; + result[serviceName] = await fetch(`${process.env[service]}/health`) + .then((res) => res.text()) + .catch((err) => err.message); + } + res.json(result); +}); diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts index a27de80..c7444c3 100644 --- a/app/server/services/ts/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -1,6 +1,5 @@ import { stmts } from "./db"; - function json(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, @@ -13,21 +12,19 @@ function json(body: unknown, status = 200) { }); } - function badRequest(message: string) { return json({ ok: false, error: message }, 400); } - function getOptions() { return { - status: 204, - headers: { - "Access-Control-Allow-Origin": "http://localhost.*", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - } + status: 204, + headers: { + "Access-Control-Allow-Origin": "http://localhost.*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET,POST,OPTIONS", + }, + }; } // ---- server ---- @@ -37,40 +34,42 @@ Bun.serve({ const url = new URL(req.url); if (req.method === "OPTIONS") { - return new Response(null, getOptions()) - }; + return new Response(null, getOptions()); + } - if(url.pathname === '/api/auth/token' && req.method === "POST") { + if (url.pathname === "/api/auth/token" && req.method === "POST") { let body; try { body = await req.json(); - if(!body.email || !body.password) { throw new Error() } - } catch { - return badRequest("Body must be JSON containing username and password."); + if (!body.email || !body.password) { + throw new Error(); + } + } catch { + return badRequest( + "Body must be JSON containing username and password.", + ); } - if(!validateEmail(body.email)) { - return + if (!validateEmail(body.email)) { + return; } - if(!validatePassword(body.password)) { - + if (!validatePassword(body.password)) { } - - - } // ---- SIGNUP ---- if (req.method === "POST" && url.pathname === "/api/auth/signup") { - const email = body.email.trim().toLowerCase(); const password = body.password; - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); - if (password.length < 8) return badRequest("Password must be at least 8 characters."); + if (!isValidEmail(email)) + return badRequest("Please enter a valid email address."); + if (password.length < 8) + return badRequest("Password must be at least 8 characters."); const existing = stmts.findByEmail.get(email) as any; - if (existing) return json({ ok: false, error: "Email already exists." }, 409); + if (existing) + return json({ ok: false, error: "Email already exists." }, 409); const passwordHash = await hashPassword(password); stmts.createUser.run(email, passwordHash); @@ -92,13 +91,16 @@ Bun.serve({ const email = body.email.trim().toLowerCase(); const password = body.password; - if (!isValidEmail(email)) return badRequest("Please enter a valid email address."); + if (!isValidEmail(email)) + return badRequest("Please enter a valid email address."); const user = stmts.findByEmail.get(email) as any; - if (!user) return json({ ok: false, error: "Invalid email or password." }, 401); + if (!user) + return json({ ok: false, error: "Invalid email or password." }, 401); const valid = await verifyPassword(password, user.password_hash); - if (!valid) return json({ ok: false, error: "Invalid email or password." }, 401); + if (!valid) + return json({ ok: false, error: "Invalid email or password." }, 401); return json({ ok: true, diff --git a/app/server/services/ts/common/corsUtil.ts b/app/server/services/ts/common/corsUtil.ts index 1b901df..31c6be7 100644 --- a/app/server/services/ts/common/corsUtil.ts +++ b/app/server/services/ts/common/corsUtil.ts @@ -1,12 +1,14 @@ -import cors from 'cors'; +import cors from "cors"; -const defaultMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']; -const defaultOrigin = 'http://localhost:8080'; -export function buildCorsConfig(opts: { origin?: string, methods?: string[] } | undefined) { - return cors({ +const defaultMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]; +const defaultOrigin = "http://localhost:8080"; +export function buildCorsConfig( + opts: { origin?: string; methods?: string[] } | undefined, +) { + return cors({ origin: opts?.origin ?? defaultOrigin, methods: opts?.methods ?? defaultMethods, credentials: true, - allowedHeaders: ['Content-Type', 'Authorization'] - }); + allowedHeaders: ["Content-Type", "Authorization"], + }); } diff --git a/app/server/services/ts/common/hooks.ts b/app/server/services/ts/common/hooks.ts index 0c6083d..aea4f13 100644 --- a/app/server/services/ts/common/hooks.ts +++ b/app/server/services/ts/common/hooks.ts @@ -1,3 +1,3 @@ export function onExit(callback: () => void) { - process.on("SIGTERM", callback); + process.on("SIGTERM", callback); } diff --git a/app/server/services/ts/common/port.ts b/app/server/services/ts/common/port.ts index 08c4243..d431974 100644 --- a/app/server/services/ts/common/port.ts +++ b/app/server/services/ts/common/port.ts @@ -1 +1 @@ -export const PORT = process.env.PORT || 8000 +export const PORT = process.env.PORT || 8000; diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json index 896ae1a..acc8ce9 100644 --- a/app/server/services/ts/package.json +++ b/app/server/services/ts/package.json @@ -1,6 +1,7 @@ { "scripts": { - "format": "bun prettier --write \"**/src/*.ts|**/*.ts\"" + "format": "bun prettier --write \"**/src/*.ts|**/*.ts\"", + "prepare": "cd ../../../../ && husky server_hooks/.husky" }, "devDependencies": { "@eslint/css": "^0.14.1", @@ -9,6 +10,7 @@ "@types/bun": "latest", "eslint": "^9", "globals": "^17.3.0", + "husky": "^9.1.7", "jiti": "^2.6.1", "minimatch": "5", "prettier": "3.8.1", diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index a783fd1..d930eb9 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -1,7 +1,7 @@ -import { PORT } from '@/port'; -import { onExit } from '@/hooks'; -import { buildCorsConfig } from '@/corsUtil'; -import express from 'express'; +import { PORT } from "@/port"; +import { onExit } from "@/hooks"; +import { buildCorsConfig } from "@/corsUtil"; +import express from "express"; const app = express(); app.use(express.json()); @@ -13,6 +13,6 @@ const server = app.listen(PORT, () => { onExit(() => server.close()); //test endpoint -app.get('/health', (req: express.Request, res: express.Response) => { - res.send('ok'); +app.get("/health", (req: express.Request, res: express.Response) => { + res.send("ok"); }); From 2e3375ea4f13b166b048d47549aa294ddafc098b Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 16 Feb 2026 10:25:03 -0600 Subject: [PATCH 050/204] git hooks to format pre-commit --- server_hooks/.husky/pre-commit | 8 ++++++++ server_hooks/navigate_to_server.sh | 1 + 2 files changed, 9 insertions(+) create mode 100644 server_hooks/.husky/pre-commit create mode 100644 server_hooks/navigate_to_server.sh diff --git a/server_hooks/.husky/pre-commit b/server_hooks/.husky/pre-commit new file mode 100644 index 0000000..fd7939e --- /dev/null +++ b/server_hooks/.husky/pre-commit @@ -0,0 +1,8 @@ +#!/bin/bash +ts_services_directory=app/server/services/ts +changed_files=$(git diff --cached --name-only --diff-filter=ACMR) +changed_server_files=$(echo $changed_files | grep -E $ts_services_directory | sed "s|$ts_services_directory/||g") +cd $ts_services_directory +bun prettier $(echo $changed_server_files | sed 's| |\\ |g') --write --ignore-unknown +git update-index --again + diff --git a/server_hooks/navigate_to_server.sh b/server_hooks/navigate_to_server.sh new file mode 100644 index 0000000..28392d4 --- /dev/null +++ b/server_hooks/navigate_to_server.sh @@ -0,0 +1 @@ +cd server/services/ts From d6a6988c643039419d07c4f1eb7012ab32c02a7d Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 16 Feb 2026 10:27:27 -0600 Subject: [PATCH 051/204] removed unused file --- server_hooks/navigate_to_server.sh | 1 - 1 file changed, 1 deletion(-) delete mode 100644 server_hooks/navigate_to_server.sh diff --git a/server_hooks/navigate_to_server.sh b/server_hooks/navigate_to_server.sh deleted file mode 100644 index 28392d4..0000000 --- a/server_hooks/navigate_to_server.sh +++ /dev/null @@ -1 +0,0 @@ -cd server/services/ts From 2aee004c79c39aa083218ccfb9ee25f1b128dc97 Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 16 Feb 2026 12:45:37 -0600 Subject: [PATCH 052/204] formatting AND eslint runs before commit AND removed auth b/c deep is working on it in another branch --- app/client/src/App.tsx | 2 +- .../services/ts/api-gateway/src/index.ts | 2 +- .../services/ts/auth/src/credentialUtils.ts | 17 --- app/server/services/ts/auth/src/index.ts | 120 ------------------ app/server/services/ts/eslint.config.mts | 24 +++- server_hooks/.husky/pre-commit | 9 +- server_hooks/format_ts_dir.sh | 13 ++ 7 files changed, 37 insertions(+), 150 deletions(-) delete mode 100644 app/server/services/ts/auth/src/credentialUtils.ts delete mode 100644 app/server/services/ts/auth/src/index.ts mode change 100644 => 100755 server_hooks/.husky/pre-commit create mode 100755 server_hooks/format_ts_dir.sh diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 522aa2f..d2d268d 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -2,7 +2,7 @@ This is to be used as a rerouter to various website pages. */ -import { Suspense, useState, lazy} from 'react' +import { Suspense, useState } from 'react' import './App.css' import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom' import LoadingSpinner from './components/ui/LoadingSpinner.tsx' diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 9c8ee0d..99c656a 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -24,7 +24,7 @@ onExit(() => server.close()); //test endpoint app.get("/health", async (req: express.Request, res: express.Response) => { - const result: { [string]: string } = {}; + const result: { [service: string]: string } = {}; const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x), ); diff --git a/app/server/services/ts/auth/src/credentialUtils.ts b/app/server/services/ts/auth/src/credentialUtils.ts deleted file mode 100644 index 2580cfa..0000000 --- a/app/server/services/ts/auth/src/credentialUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -function validatePassword(password: string): boolean { - return true; -} - -function validateEmail(email: string): boolean { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); -} - -async function hashPassword(password: string) { - // return await Bun.password.hash(password); -} - -async function verifyPassword(password: string, hash: string) { - // return await Bun.password.verify(password, hash); -} - -export { validatePassword, validateEmail }; diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts deleted file mode 100644 index c7444c3..0000000 --- a/app/server/services/ts/auth/src/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { stmts } from "./db"; - -function json(body: unknown, status = 200) { - return new Response(JSON.stringify(body), { - status, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "http://localhost:5173", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - }); -} - -function badRequest(message: string) { - return json({ ok: false, error: message }, 400); -} - -function getOptions() { - return { - status: 204, - headers: { - "Access-Control-Allow-Origin": "http://localhost.*", - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS", - }, - }; -} - -// ---- server ---- -Bun.serve({ - port: PORT, - async fetch(req) { - const url = new URL(req.url); - - if (req.method === "OPTIONS") { - return new Response(null, getOptions()); - } - - if (url.pathname === "/api/auth/token" && req.method === "POST") { - let body; - try { - body = await req.json(); - if (!body.email || !body.password) { - throw new Error(); - } - } catch { - return badRequest( - "Body must be JSON containing username and password.", - ); - } - - if (!validateEmail(body.email)) { - return; - } - if (!validatePassword(body.password)) { - } - } - - // ---- SIGNUP ---- - if (req.method === "POST" && url.pathname === "/api/auth/signup") { - const email = body.email.trim().toLowerCase(); - const password = body.password; - - if (!isValidEmail(email)) - return badRequest("Please enter a valid email address."); - if (password.length < 8) - return badRequest("Password must be at least 8 characters."); - - const existing = stmts.findByEmail.get(email) as any; - if (existing) - return json({ ok: false, error: "Email already exists." }, 409); - - const passwordHash = await hashPassword(password); - stmts.createUser.run(email, passwordHash); - - return json({ ok: true, message: "Account created." }, 201); - } - - // ---- LOGIN ---- - if (req.method === "POST" && url.pathname === "/api/auth/login") { - const body = (await req.json().catch(() => null)) as null | { - email?: string; - password?: string; - }; - - if (!body?.email || !body?.password) { - return badRequest("Email and password are required."); - } - - const email = body.email.trim().toLowerCase(); - const password = body.password; - - if (!isValidEmail(email)) - return badRequest("Please enter a valid email address."); - - const user = stmts.findByEmail.get(email) as any; - if (!user) - return json({ ok: false, error: "Invalid email or password." }, 401); - - const valid = await verifyPassword(password, user.password_hash); - if (!valid) - return json({ ok: false, error: "Invalid email or password." }, 401); - - return json({ - ok: true, - message: "Logged in!", - user: { id: user.id, email: user.email }, - }); - } - - if (req.method === "GET" && url.pathname === "/api/health") { - return json({ ok: true, status: "up" }); - } - - return json({ ok: false, error: "Not found" }, 404); - }, -}); - -console.log(`Auth service running on ${PORT}`); diff --git a/app/server/services/ts/eslint.config.mts b/app/server/services/ts/eslint.config.mts index cd2f864..d36a5e7 100644 --- a/app/server/services/ts/eslint.config.mts +++ b/app/server/services/ts/eslint.config.mts @@ -3,11 +3,27 @@ import globals from "globals"; import tseslint from "typescript-eslint"; import json from "@eslint/json"; import css from "@eslint/css"; -import { defineConfig } from "eslint/config"; +import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, tseslint.configs.recommended, - { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] }, - { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] }, + globalIgnores(["tsconfig.json"]), + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], + plugins: { js }, + extends: ["js/recommended"], + languageOptions: { globals: globals.node }, + }, + { + files: ["**/*.json"], + plugins: { json }, + language: "json/json", + extends: ["json/recommended"], + }, + { + files: ["**/*.css"], + plugins: { css }, + language: "css/css", + extends: ["css/recommended"], + }, ]); diff --git a/server_hooks/.husky/pre-commit b/server_hooks/.husky/pre-commit old mode 100644 new mode 100755 index fd7939e..68ffd8d --- a/server_hooks/.husky/pre-commit +++ b/server_hooks/.husky/pre-commit @@ -1,8 +1,3 @@ #!/bin/bash -ts_services_directory=app/server/services/ts -changed_files=$(git diff --cached --name-only --diff-filter=ACMR) -changed_server_files=$(echo $changed_files | grep -E $ts_services_directory | sed "s|$ts_services_directory/||g") -cd $ts_services_directory -bun prettier $(echo $changed_server_files | sed 's| |\\ |g') --write --ignore-unknown -git update-index --again - +./server_hooks/format_ts_dir.sh app/server/services/ts +./server_hooks/format_ts_dir.sh app/client diff --git a/server_hooks/format_ts_dir.sh b/server_hooks/format_ts_dir.sh new file mode 100755 index 0000000..0795180 --- /dev/null +++ b/server_hooks/format_ts_dir.sh @@ -0,0 +1,13 @@ +#!/bin/bash +#first argument ($1) is the directory to format the files of. +changed_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E $1/) +normalized_files=$(echo $changed_files | sed "s|$1/||g" ) +cd $1 +bun eslint $changed_dir_files +if [[ $? != 0 ]]; then + echo "❌ Failed to format files" + exit 1 +fi +bun prettier $changed_dir_files --write --ignore-unknown +git update-index --again + From 651e1fd1bdebdfd33c5e204c476149900cc8d1c3 Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 16 Feb 2026 12:46:10 -0600 Subject: [PATCH 053/204] pretty client --- app/client/bun.lockb | Bin 87858 -> 88226 bytes app/client/package.json | 1 + 2 files changed, 1 insertion(+) diff --git a/app/client/bun.lockb b/app/client/bun.lockb index dc4aac6446390edf34d9674d15f3a421306674f1..8d1fd68b4b1d5aabacd82ba32c2d992390fb9695 100755 GIT binary patch delta 7428 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 WeCSj=KO!Q%uIbu;gqrz?IP_mWehVuA delta 7274 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 Date: Mon, 16 Feb 2026 12:56:06 -0600 Subject: [PATCH 054/204] changed name of hooks folder --- app/server/services/ts/package.json | 2 +- hooks/.husky/pre-commit | 4 ++++ {server_hooks => hooks}/format_ts_dir.sh | 0 server_hooks/.husky/pre-commit | 3 --- 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100755 hooks/.husky/pre-commit rename {server_hooks => hooks}/format_ts_dir.sh (100%) delete mode 100755 server_hooks/.husky/pre-commit diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json index acc8ce9..8eee956 100644 --- a/app/server/services/ts/package.json +++ b/app/server/services/ts/package.json @@ -1,7 +1,7 @@ { "scripts": { "format": "bun prettier --write \"**/src/*.ts|**/*.ts\"", - "prepare": "cd ../../../../ && husky server_hooks/.husky" + "prepare": "cd ../../../../ && husky hooks/.husky" }, "devDependencies": { "@eslint/css": "^0.14.1", diff --git a/hooks/.husky/pre-commit b/hooks/.husky/pre-commit new file mode 100755 index 0000000..69d45cd --- /dev/null +++ b/hooks/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/bash +echo "🔎 Checking files..." +./hooks/format_ts_dir.sh app/server/services/ts +./hooks/format_ts_dir.sh app/client diff --git a/server_hooks/format_ts_dir.sh b/hooks/format_ts_dir.sh similarity index 100% rename from server_hooks/format_ts_dir.sh rename to hooks/format_ts_dir.sh diff --git a/server_hooks/.husky/pre-commit b/server_hooks/.husky/pre-commit deleted file mode 100755 index 68ffd8d..0000000 --- a/server_hooks/.husky/pre-commit +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -./server_hooks/format_ts_dir.sh app/server/services/ts -./server_hooks/format_ts_dir.sh app/client From 52e896644b1e1c69565361ceecbda645a912e70a Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Mon, 16 Feb 2026 16:10:51 -0600 Subject: [PATCH 055/204] Updated User Authentication service to work with updated changes --- app/client/bun.lockb | Bin 87858 -> 88226 bytes app/client/package.json | 1 + app/client/src/App.css | 49 ++ app/client/src/App.tsx | 181 ++++--- app/server/database/schema.sql | 153 ------ app/server/db_password.txt | 2 +- app/server/db_root_password.txt | 2 +- app/server/docker-compose.yml | 114 ++--- app/server/services/api-gateway/.gitignore | 34 -- app/server/services/api-gateway/Dockerfile | 12 - app/server/services/api-gateway/README.md | 15 - app/server/services/api-gateway/bun.lock | 216 --------- app/server/services/api-gateway/bun.lockb | Bin 121512 -> 0 bytes .../services/api-gateway/eslint.config.mts | 11 - app/server/services/api-gateway/package.json | 28 -- app/server/services/api-gateway/src/index.ts | 93 ---- app/server/services/api-gateway/tsconfig.json | 29 -- app/server/services/auth/.gitignore | 34 -- app/server/services/auth/Dockerfile | 12 - app/server/services/auth/README.md | 15 - app/server/services/auth/bun.lock | 223 --------- app/server/services/auth/bun.lockb | Bin 123198 -> 0 bytes app/server/services/auth/eslint.config.ts | 11 - app/server/services/auth/package.json | 35 -- .../services/auth/src/credentialUtils.ts | 21 - app/server/services/auth/src/db.ts | 212 --------- app/server/services/auth/src/index.ts | 150 ------ app/server/services/auth/tsconfig.json | 29 -- app/server/services/database/schema.sql | 139 +++++- .../{ => services}/database/updateSchema.sh | 0 app/server/services/python/.gitignore | 175 +++++++ .../{ => python}/analytics/Dockerfile | 0 .../{ => python}/analytics/requirements.txt | 0 .../{ => python}/analytics/src/main.py | 0 .../services/{ => python}/market/Dockerfile | 0 .../{ => python}/market/requirements.txt | 0 .../services/{ => python}/market/src/main.py | 0 app/server/services/ts/.gitignore | 179 +++++++ app/server/services/ts/README.md | 10 + app/server/services/ts/api-gateway/Dockerfile | 15 + .../services/ts/api-gateway/package.json | 9 + .../services/ts/api-gateway/src/index.ts | 75 +++ app/server/services/ts/auth/Dockerfile | 14 + app/server/services/ts/auth/package.json | 13 + app/server/services/ts/auth/src/index.ts | 446 ++++++++++++++++++ app/server/services/ts/common/corsUtil.ts | 14 + app/server/services/ts/common/hooks.ts | 3 + app/server/services/ts/common/port.ts | 1 + app/server/services/ts/eslint.config.mts | 29 ++ app/server/services/ts/package.json | 26 + app/server/services/ts/tsconfig.json | 48 ++ app/server/services/ts/user/Dockerfile | 14 + app/server/services/ts/user/package.json | 9 + app/server/services/ts/user/src/index.ts | 18 + app/server/services/user/.gitignore | 34 -- app/server/services/user/Dockerfile | 12 - app/server/services/user/README.md | 15 - app/server/services/user/bun.lock | 179 ------- app/server/services/user/bun.lockb | Bin 115888 -> 0 bytes app/server/services/user/eslint.config.ts | 11 - app/server/services/user/package.json | 30 -- app/server/services/user/src/index.ts | 15 - app/server/services/user/tsconfig.json | 29 -- 63 files changed, 1460 insertions(+), 1784 deletions(-) delete mode 100644 app/server/database/schema.sql delete mode 100644 app/server/services/api-gateway/.gitignore delete mode 100644 app/server/services/api-gateway/Dockerfile delete mode 100644 app/server/services/api-gateway/README.md delete mode 100644 app/server/services/api-gateway/bun.lock delete mode 100755 app/server/services/api-gateway/bun.lockb delete mode 100644 app/server/services/api-gateway/eslint.config.mts delete mode 100644 app/server/services/api-gateway/package.json delete mode 100644 app/server/services/api-gateway/src/index.ts delete mode 100644 app/server/services/api-gateway/tsconfig.json delete mode 100644 app/server/services/auth/.gitignore delete mode 100644 app/server/services/auth/Dockerfile delete mode 100644 app/server/services/auth/README.md delete mode 100644 app/server/services/auth/bun.lock delete mode 100755 app/server/services/auth/bun.lockb delete mode 100644 app/server/services/auth/eslint.config.ts delete mode 100644 app/server/services/auth/package.json delete mode 100644 app/server/services/auth/src/credentialUtils.ts delete mode 100644 app/server/services/auth/src/db.ts delete mode 100644 app/server/services/auth/src/index.ts delete mode 100644 app/server/services/auth/tsconfig.json rename app/server/{ => services}/database/updateSchema.sh (100%) mode change 100755 => 100644 create mode 100644 app/server/services/python/.gitignore rename app/server/services/{ => python}/analytics/Dockerfile (100%) rename app/server/services/{ => python}/analytics/requirements.txt (100%) rename app/server/services/{ => python}/analytics/src/main.py (100%) rename app/server/services/{ => python}/market/Dockerfile (100%) rename app/server/services/{ => python}/market/requirements.txt (100%) rename app/server/services/{ => python}/market/src/main.py (100%) create mode 100644 app/server/services/ts/.gitignore create mode 100644 app/server/services/ts/README.md create mode 100644 app/server/services/ts/api-gateway/Dockerfile create mode 100644 app/server/services/ts/api-gateway/package.json create mode 100644 app/server/services/ts/api-gateway/src/index.ts create mode 100644 app/server/services/ts/auth/Dockerfile create mode 100644 app/server/services/ts/auth/package.json create mode 100644 app/server/services/ts/auth/src/index.ts create mode 100644 app/server/services/ts/common/corsUtil.ts create mode 100644 app/server/services/ts/common/hooks.ts create mode 100644 app/server/services/ts/common/port.ts create mode 100644 app/server/services/ts/eslint.config.mts create mode 100644 app/server/services/ts/package.json create mode 100644 app/server/services/ts/tsconfig.json create mode 100644 app/server/services/ts/user/Dockerfile create mode 100644 app/server/services/ts/user/package.json create mode 100644 app/server/services/ts/user/src/index.ts delete mode 100644 app/server/services/user/.gitignore delete mode 100644 app/server/services/user/Dockerfile delete mode 100644 app/server/services/user/README.md delete mode 100644 app/server/services/user/bun.lock delete mode 100755 app/server/services/user/bun.lockb delete mode 100644 app/server/services/user/eslint.config.ts delete mode 100644 app/server/services/user/package.json delete mode 100644 app/server/services/user/src/index.ts delete mode 100644 app/server/services/user/tsconfig.json diff --git a/app/client/bun.lockb b/app/client/bun.lockb index dc4aac6446390edf34d9674d15f3a421306674f1..8d1fd68b4b1d5aabacd82ba32c2d992390fb9695 100755 GIT binary patch delta 7428 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 WeCSj=KO!Q%uIbu;gqrz?IP_mWehVuA delta 7274 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: Promise { +async function requestAuth(path: string, payload: Record): Promise { try { const response = await fetch(`${API_BASE_URL}${path}`, { method: 'POST', @@ -240,6 +244,7 @@ function SignUpPage({ onSignup }: SignUpPageProps) { const [username, setUsername] = useState('') const [firstName, setFirstName] = useState('') const [lastName, setLastName] = useState('') + const [age, setAge] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') @@ -253,10 +258,16 @@ function SignUpPage({ onSignup }: SignUpPageProps) { const cleanedUsername = username.trim() const cleanedFirstName = firstName.trim() const cleanedLastName = lastName.trim() + const parsedAge = Number(age) const normalizedEmail = email.trim().toLowerCase() - if (!cleanedUsername || !cleanedFirstName || !cleanedLastName) { - setErrorMessage('Username, first name, and last name are required.') + if (!cleanedUsername || !cleanedFirstName || !cleanedLastName || !age.trim()) { + setErrorMessage('Username, first name, last name, and age are required.') + return + } + + if (!Number.isInteger(parsedAge) || parsedAge < MIN_AGE || parsedAge > MAX_AGE) { + setErrorMessage(`Age must be a whole number between ${MIN_AGE} and ${MAX_AGE}.`) return } @@ -270,6 +281,7 @@ function SignUpPage({ onSignup }: SignUpPageProps) { username: cleanedUsername, firstName: cleanedFirstName, lastName: cleanedLastName, + age: parsedAge, email: normalizedEmail, password, }) @@ -288,71 +300,107 @@ function SignUpPage({ onSignup }: SignUpPageProps) { return (
-
+

Finus

Create account

Set up your account in less than a minute.

-
- - setUsername(event.target.value)} - placeholder="alexp" - required - /> - - - setFirstName(event.target.value)} - placeholder="Alex" - required - /> - - - setLastName(event.target.value)} - placeholder="Patel" - required - /> - - - setEmail(event.target.value)} - placeholder="you@example.com" - required - /> - - - setPassword(event.target.value)} - placeholder="At least 8 characters" - required - /> - - - setConfirmPassword(event.target.value)} - placeholder="Repeat password" - required - /> + +
+ Profile +
+
+ + setUsername(event.target.value)} + placeholder="johnd" + required + /> +
+ +
+ + setFirstName(event.target.value)} + placeholder="John" + required + /> +
+ +
+ + setLastName(event.target.value)} + placeholder="Doe" + required + /> +
+ +
+ + setAge(event.target.value)} + placeholder="21" + required + /> +
+
+
+ +
+ Account +
+
+ + setEmail(event.target.value)} + placeholder="you@example.com" + required + /> +
+ +
+ + setPassword(event.target.value)} + placeholder="At least 8 characters" + required + /> +
+ +
+ + setConfirmPassword(event.target.value)} + placeholder="Repeat password" + required + /> +
+
+
{errorMessage ?

{errorMessage}

: null} @@ -381,7 +429,8 @@ function DashboardPage({ session, onLogout }: DashboardPageProps) {

Finus

Signed in

- Hello {session.user.name}. You are logged in as {session.user.email}. + Hello {session.user.name}. You are logged in as{' '} + {session.user.email}.

+
+ ); +} diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx new file mode 100644 index 0000000..474e14e --- /dev/null +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -0,0 +1,60 @@ +// Component to handle file upload, validation, and error display for CSV files +//handles one csv file at a time +import React, { useState } from "react"; +import CsvDrop from "./CsvDrop"; +import CsvFile from "./CsvFile"; +import CsvError from "./CsvError"; + +export default function CsvUpload() { + const [file, setFile] = useState(null); // selected CSV file + const [error, setError] = useState(null); // validation errors + + + + // File Validation + const handleFileSelect = (selected: File | null) => { + if (!selected) return; + + // Validate type + if (!selected.name.endsWith(".csv")) { + setError("Please upload a CSV file."); + setFile(null); + return; + } + + // Validate size + if (selected.size === 0) { + setError("File is empty."); + setFile(null); + return; + } + + if (selected.size > 5 * 1024 * 1024) { + setError("File is too large (max 5MB)."); + setFile(null); + return; + } + + setError(null); + setFile(selected); + }; + + const clearFile = () => { + setFile(null); + setError(null); + }; + + return ( +
+ + + {error && } + + {file && } + + +
+ ); +} From e02db18cb66e7987dc955ecc457f77f63d266b48 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Tue, 17 Feb 2026 13:38:32 -0600 Subject: [PATCH 060/204] Added drop boxes for Account and transition forms --- app/client/src/components/ui/AccountForm.tsx | 3 ++- app/client/src/components/ui/TransationForm.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index a6e6498..58deb6b 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import CsvUpload from "../csvread/CsvUpload"; interface popupProp{ toggle: () => void; @@ -61,7 +62,7 @@ export default function popupForm({toggle, edit}:popupProp,){

- +

diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index cd1b8c8..e342bc6 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import CsvUpload from "../csvread/CsvUpload"; interface popupProp{ toggle: () => void; @@ -65,7 +66,7 @@ export default function popupForm({toggle, edit}:popupProp){ setAmount(Number(event.target.value))}/>

- +

From 382610735db99c5883ecfe3e0bc436391ae64b46 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Tue, 17 Feb 2026 17:05:23 -0600 Subject: [PATCH 061/204] Client: added synthetic data and API skeleton for chart components --- app/client/src/api/ManagerAPI.tsx | 85 ++++++++ app/client/src/api/TransactionService.tsx | 18 -- app/client/src/components/Button.tsx | 64 ++++++ app/client/src/pages/DashboardPage.tsx | 227 ++++++++++++++++++---- app/client/src/utils/constants.ts | 2 +- 5 files changed, 339 insertions(+), 57 deletions(-) create mode 100644 app/client/src/api/ManagerAPI.tsx delete mode 100644 app/client/src/api/TransactionService.tsx create mode 100644 app/client/src/components/Button.tsx diff --git a/app/client/src/api/ManagerAPI.tsx b/app/client/src/api/ManagerAPI.tsx new file mode 100644 index 0000000..9a1cfd9 --- /dev/null +++ b/app/client/src/api/ManagerAPI.tsx @@ -0,0 +1,85 @@ +import axios from "axios"; +import { BASE_URL } from "@/utils/constants"; +import type { ChartData } from "chart.js"; +import type React from "react"; +// import type { Transaction } from "@/types/Transaction"; + +// async function getTransactions(): Promise { +// try { +// const response = await axios.get(`${BASE_URL}/api/transactions`); +// if (response.status !== 200) { +// throw new Error(`Failed to fetch transactions: ${response.statusText}`); +// } +// return response.data; +// } catch (error) { +// console.error("Error fetching transactions:", error); +// throw error; +// } +// } + + + +//API for calling for various charts below + +// Expenses over time chart +// Accepted time periods are "w" for weekly, "m" for monthly, and "y" for yearly - the backend will handle the units +async function getExpensesChartData(period: string): Promise> { + try { + if (!["w", "m", "y"].includes(period)) { + throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); + } + const response = await axios.get(`${BASE_URL}/api/charts/expenses?period=${period}`); + if (response.status !== 200) { + throw new Error(`Failed to fetch expenses chart data: ${response.statusText}`); + } + + return response.data; + } catch (error) { + console.error("Error fetching expenses chart data:", error); + throw error; + }; +} + + +// Savings contribution chart +async function getSavingsContribChartData(period: string): Promise { + try { + if (!["w", "m", "y"].includes(period)) { + throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); + } + const response = await axios.get(`${BASE_URL}/api/charts/savings-contrib?period=${period}`); + if (response.status !== 200) { + throw new Error(`Failed to fetch savings contribution chart data: ${response.statusText}`); + } + return response.data; + } catch (error) { + console.error("Error fetching savings contribution chart data:", error); + throw error; + }; +} + + +// Income flow - sankey chart data +async function getIncomeFlowChartData(period: string): Promise { + try { + if (!["w", "m", "y"].includes(period)) { + throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); + } + const response = await axios.get(`${BASE_URL}/api/charts/income-flow?period=${period}`); + if (response.status !== 200) { + throw new Error(`Failed to fetch income flow chart data: ${response.statusText}`); + } + return response.data; + } catch (error) { + console.error("Error fetching income flow chart data:", error); + throw error; + }; +} + + +export { + // getTransactions, + getExpensesChartData, + getSavingsContribChartData, + getIncomeFlowChartData +}; \ No newline at end of file diff --git a/app/client/src/api/TransactionService.tsx b/app/client/src/api/TransactionService.tsx deleted file mode 100644 index 661e654..0000000 --- a/app/client/src/api/TransactionService.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import axios from "axios"; -import { BASE_URL } from "@/utils/constants"; -import type { Transaction } from "@/types/Transaction"; -async function getTransactions(): Promise { - try { - const response = await axios.get(`${BASE_URL}/api/transactions`); - if (response.status !== 200) { - throw new Error(`Failed to fetch transactions: ${response.statusText}`); - } - return response.data; - } catch (error) { - console.error("Error fetching transactions:", error); - throw error; - } -} -export default { - getTransactions, -}; \ No newline at end of file diff --git a/app/client/src/components/Button.tsx b/app/client/src/components/Button.tsx new file mode 100644 index 0000000..b5ea4ab --- /dev/null +++ b/app/client/src/components/Button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 070688e..f6d31a0 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -1,11 +1,27 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import type { Transaction } from '@/types/Transaction' -import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; -import { Pie, Line } from 'react-chartjs-2'; +import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, type ChartData, type LineController } from 'chart.js'; +import { Pie, Line, Bar } from 'react-chartjs-2'; import AccountCard from '@/components/AccountCard'; import TransactionTable from '@/components/TransactionTable'; +import {Button} from '@/components/button'; +import LoadingSpinner from '@/components/LoadingSpinner'; +import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + + + function DashboardPage() { + //Active chart state - this just determines which chart is displayed in the holder - change this later to potentially load up all charts at once if latency is good + const [activeChart, setActiveChart] = useState<'expenses' | 'savings' | 'income'>('expenses'); + + //Active chart period state - this determines over what timeframe the chart is displayed - weekly, monthly, or yearly - default to monthly + const [selectedPeriod, setSelectedPeriod] = useState<'w' | 'm' | 'y'>('m'); + + //Chart data state - this is used to determine whether to put a spinner in place of a chart while data is being fetched from API + const [expensesData, setExpensesData] = useState | null>(null); + const [isLoading, setIsLoading] = useState(false); + const data = { labels: [ 'Red', @@ -24,6 +40,8 @@ function DashboardPage() { circumference: 360, }] }; + + const options = { responsive: true, plugins: { @@ -49,48 +67,172 @@ function DashboardPage() { }, }; + useEffect(() => { + const fetchExpensesData = async () => { + if (activeChart !== 'expenses') return; + + setIsLoading(true); + try { + const data = await getTestExpensesData(selectedPeriod); + setExpensesData(data); + } catch (error) { + console.error("Failed to fetch expenses data:", error); + } finally { + setIsLoading(false); + } + }; - const linearOptions = { - responsive: true, - plugins: { - legend: { - position: 'top' as const, - }, - title: { - display: true, - text: 'Income vs Expenses', - font: { - size: 24, - weight: 'bold' as const - } - }, - }, + fetchExpensesData(); + }, [activeChart, selectedPeriod]); + + + //Temporary test data for expenses - delete once API works. This is just to test graph components + const getTestExpensesData = async (period: 'w' | 'm' | 'y'): Promise> => { + //Try to reach API first, get synthetic data if fails + try{ + const response = await getExpensesChartData(period); + return response; + }catch(error) { + console.error("Error fetching expenses chart data:", error); + } + switch(period) { + case 'w': + return { + labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + datasets: [{ + label: 'Weekly Expenses', + data: [125, 89, 210, 45, 167, 92, 78], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }] + }; + + case 'm': + return { + labels: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan', '11 Jan', '12 Jan', + '13 Jan', '14 Jan', '15 Jan', '16 Jan', '17 Jan', '18 Jan', '19 Jan', '20 Jan', '21 Jan', '22 Jan', '23 Jan', '24 Jan', + '25 Jan', '26 Jan', '27 Jan', '28 Jan', '29 Jan', '30 Jan', '31 Jan' + ], + datasets: [{ + label: 'Monthly Expenses', + data: [125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, 65, 190, 120, + 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, 143, 87 + ], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }] + }; + + case 'y': + return { + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + datasets: [{ + label: 'Yearly Expenses', + data: [3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, 4123], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }] + }; + } }; - const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; +const expensesBarOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Expenses Over Time', + font: { + size: 24, + weight: 'bold' as const + } + } + }, +}; - const linearData = { - labels, - datasets: [ - { - label: 'Income', - data: ["2374823", "83932", "8294","102190"], - borderColor: 'rgb(255, 99, 132)', - backgroundColor: 'rgba(255, 99, 132, 0.5)', - }, - { - label: 'Expenses', - data: ["2374", "9938", "122","8723"], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }, - ], +const savingsLineOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Expenses Over Time', + font: { + size: 24, + weight: 'bold' as const + } + } + }, +}; + +const incomeSankeyOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Expenses Over Time', + font: { + size: 24, + weight: 'bold' as const + } + } + }, +}; + + +//Renders a chart based on activeChart - puts a spinner in place while data is being fetched or if no data is available for any reason + const renderChart = () => { + switch(activeChart) { + case 'expenses': + if (isLoading || !expensesData) { + return ( +
+ < LoadingSpinner /> +
+ ); + } + return ( +
+ +
+ ); + case 'savings': + return ( +
+ {/* savingsLineOptions */} +

Savings Chart (Coming Soon)

+
+ ); + case 'income': + return ( +
+ +

Income Flow Chart (Coming Soon)

+
+ ); + default: + return ( +
+ {/* figure out a default case in case of an error - can have a placeholder or a spinner chart (loading spinner*/} + < LoadingSpinner /> +
+ ); + } }; return ( -
+
+ {/*p-10 bg-gray-100 */}

Welcome Username

-

Here you can view your recent transactions and manage your finances.

+

Here you can view your recent transactions and manage your finances.

@@ -98,9 +240,18 @@ function DashboardPage() {
+
+ + + +
- + {/* This chart is here just to test all the graph components */} + + + + {renderChart()}
diff --git a/app/client/src/utils/constants.ts b/app/client/src/utils/constants.ts index b95b2f7..ea4357f 100644 --- a/app/client/src/utils/constants.ts +++ b/app/client/src/utils/constants.ts @@ -1 +1 @@ -export const BASE_URL = 'https://api.example.com'; \ No newline at end of file +export const BASE_URL = 'http://localhost:3000';//change this to the public link once microservices are hosted on AWS \ No newline at end of file From 2b3008cd8d92b781a96e44fb09091d5e42a443c3 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Tue, 17 Feb 2026 17:33:39 -0600 Subject: [PATCH 062/204] initi papaparse for csv reading --- app/client/bun.lock | 3 +++ app/client/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/app/client/bun.lock b/app/client/bun.lock index a786de4..e67e7be 100644 --- a/app/client/bun.lock +++ b/app/client/bun.lock @@ -6,6 +6,7 @@ "name": "finus-client", "dependencies": { "@types/react-router-dom": "^5.3.3", + "papaparse": "^5.5.3", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", @@ -382,6 +383,8 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], diff --git a/app/client/package.json b/app/client/package.json index 151585f..d22961c 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@types/react-router-dom": "^5.3.3", + "papaparse": "^5.5.3", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0" From 7b48f4abf8a286c6138d25c603999a5384b2caff Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Tue, 17 Feb 2026 17:42:03 -0600 Subject: [PATCH 063/204] Correction on init for papaparse lib --- app/client/bun.lock | 3 +++ app/client/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/app/client/bun.lock b/app/client/bun.lock index e67e7be..5dfad89 100644 --- a/app/client/bun.lock +++ b/app/client/bun.lock @@ -14,6 +14,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", + "@types/papaparse": "^5.5.2", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.2", @@ -177,6 +178,8 @@ "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="], + "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], diff --git a/app/client/package.json b/app/client/package.json index d22961c..03d7fbc 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", + "@types/papaparse": "^5.5.2", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.2", From 38b616a0e1fa5578b7adf47d022674a29ff463fc Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 19 Feb 2026 09:50:15 -0600 Subject: [PATCH 064/204] created an utility parser for csv files using papaparse --- app/client/src/util/ParseCsv.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/client/src/util/ParseCsv.ts diff --git a/app/client/src/util/ParseCsv.ts b/app/client/src/util/ParseCsv.ts new file mode 100644 index 0000000..76f1a3f --- /dev/null +++ b/app/client/src/util/ParseCsv.ts @@ -0,0 +1,24 @@ +//this utility parses csv files using papaparse +import Papa from "papaparse"; + +export interface ParsedRow { + [key: string]: string | number | null; +} +//function takes a file object and returns an array of parsed rows using promise and papaparse lib. +export function parseCsvFile(file: File): Promise { + return new Promise((resolve, reject) => { + Papa.parse(file, { + header: true, + skipEmptyLines: true, + dynamicTyping: true, + complete: (results) => { + if (results.errors.length > 0) { + reject(results.errors[0].message); + } else { + resolve(results.data as ParsedRow[]); + } + }, + error: (err) => reject(err.message), + }); + }); +} From 58d1d03049c0566f99e507ec3030f0671b94c5d5 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 19 Feb 2026 10:37:02 -0600 Subject: [PATCH 065/204] Utility files for Normalization and validation on csv entry --- app/client/src/util/NormalizeRow.ts | 59 +++++++++++++++++++++++++++++ app/client/src/util/ValidateFile.ts | 21 ++++++++++ app/client/src/util/ValidateRow.ts | 24 ++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 app/client/src/util/NormalizeRow.ts create mode 100644 app/client/src/util/ValidateFile.ts create mode 100644 app/client/src/util/ValidateRow.ts diff --git a/app/client/src/util/NormalizeRow.ts b/app/client/src/util/NormalizeRow.ts new file mode 100644 index 0000000..5f2fa60 --- /dev/null +++ b/app/client/src/util/NormalizeRow.ts @@ -0,0 +1,59 @@ +// Utility function to normalize row data in a CSV file + +export interface NormalizedRow { + date: string | null; + description: string | null; + amount: number | null; + sender?: string | null; + recipient?: string | null; +} + +export function normalizeRow(raw: unknown): NormalizedRow { //row is treated as an object with unknown values + const record = raw as Record; + + const clean = (value: unknown): string | null => { // helper to trim and clean string-like values + if (value === undefined || value === null) return null; + return String(value).trim() || null; + }; + const rawDate = clean(record.date); // normalize fields using helpers + const date = normalizeDate(rawDate); + const rawAmount = clean(record.amount); + const amount = normalizeAmount(rawAmount); + + return { + date, + description: clean(record.description), + amount, + sender: clean(record.sender), + recipient: clean(record.recipient), + }; +} + +// helper to normalize date formats into YYYY-MM-DD +function normalizeDate(input: string | null): string | null { + if (!input) return null; + const data = input.replace(/[.\s]/g, "-").replace(/\//g, "-"); + if (/^\d{4}-\d{2}-\d{2}$/.test(data)) return data; // already in YYYY-MM-DD + + if (/^\d{2}-\d{2}-\d{4}$/.test(data)) { // if MM-DD-YYYY or DD-MM-YYYY, convert to YYYY-MM-DD + const [m, d, y] = data.split("-"); + return `${y}-${m}-${d}`; + } + if (/^\d{2}-\d{2}-\d{4}$/.test(data)) { + const [d, m, y] = data.split("-"); + return `${y}-${m}-${d}`; + } + + return null; // invalid date +} + +// helper to normalize amount fields +function normalizeAmount(input: string | null): number | null { + if (!input) return null; + let amount = input.replace(/,/g, "").trim(); //trim + if (/^\(.+\)$/.test(amount)) { //if amount is in parentheses, treat as negative + amount = "-" + amount.slice(1, -1); + } + const num = Number(amount); + return isNaN(num) ? null : num; +} diff --git a/app/client/src/util/ValidateFile.ts b/app/client/src/util/ValidateFile.ts new file mode 100644 index 0000000..358331d --- /dev/null +++ b/app/client/src/util/ValidateFile.ts @@ -0,0 +1,21 @@ +// Utility function to validate the entire CSV file + +import type { TransactionDraft } from "./ConvertTransaction"; + +export interface FileValidationResult { // summary of validation results for the whole file + validCount: number; + invalidCount: number; + tooManyInvalid: boolean; +} + +export function validateFile(rows: TransactionDraft[]): FileValidationResult { //returns a summary of validation results for the whole file + const validCount = rows.filter(r => r.errors.length === 0).length; + const invalidCount = rows.length - validCount; + const tooManyInvalid = rows.length > 0 && invalidCount / rows.length > 0.2; //if more than 20% of the rows are invalid we send a warning + + return { + validCount, + invalidCount, + tooManyInvalid, + }; +} diff --git a/app/client/src/util/ValidateRow.ts b/app/client/src/util/ValidateRow.ts new file mode 100644 index 0000000..19620bd --- /dev/null +++ b/app/client/src/util/ValidateRow.ts @@ -0,0 +1,24 @@ +// Utility function to validate a normalized CSV row + +import type { NormalizedRow } from "./NormalizeRow"; + +export function validateRow(row: NormalizedRow): string[] { + const errors: string[] = []; + + // validate date + if (!row.date) { + errors.push("Invalid or missing date"); + } + + // validate description + if (!row.description) { + errors.push("Missing description"); + } + + // validate amount + if (row.amount === null) { + errors.push("Invalid or missing amount"); + } + + return errors; +} From 20e42fce206384bcf463098a630bea872cf429eb Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 19 Feb 2026 10:40:13 -0600 Subject: [PATCH 066/204] Implemented a convert row -> transaction draft and updated ParseCsv to use new file instead --- app/client/src/util/ConvertTransaction.ts | 24 +++++++++++++++++ app/client/src/util/ParseCsv.ts | 32 ++++++++++++++--------- 2 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 app/client/src/util/ConvertTransaction.ts diff --git a/app/client/src/util/ConvertTransaction.ts b/app/client/src/util/ConvertTransaction.ts new file mode 100644 index 0000000..c7983b5 --- /dev/null +++ b/app/client/src/util/ConvertTransaction.ts @@ -0,0 +1,24 @@ +//function to convert a normalized row into a transaction draft + + +import type { NormalizedRow } from "./NormalizeRow"; +import { validateRow } from "./ValidateRow"; + +export interface TransactionDraft { + date: string | null; + description: string | null; + amount: number | null; + sender?: string | null; + recipient?: string | null; + errors: string[]; +} + +export function convertDraft(row: NormalizedRow): TransactionDraft { // convert normalized row to transaction draft and validate it + const errors = validateRow(row); + + return { + ...row, + errors, + }; +} + diff --git a/app/client/src/util/ParseCsv.ts b/app/client/src/util/ParseCsv.ts index 76f1a3f..139c413 100644 --- a/app/client/src/util/ParseCsv.ts +++ b/app/client/src/util/ParseCsv.ts @@ -1,24 +1,32 @@ //this utility parses csv files using papaparse +// Parses CSV, normalizes rows, validates them, and returns TransactionDraft[] + import Papa from "papaparse"; +import { normalizeRow } from "./NormalizeRow"; +import { convertDraft, type TransactionDraft } from "./ConvertTransaction"; -export interface ParsedRow { - [key: string]: string | number | null; -} -//function takes a file object and returns an array of parsed rows using promise and papaparse lib. -export function parseCsvFile(file: File): Promise { - return new Promise((resolve, reject) => { +export function parseCsvFile(file: File): Promise { //uses promises to handle async parsing + return new Promise((resolve, reject) => { Papa.parse(file, { header: true, skipEmptyLines: true, dynamicTyping: true, - complete: (results) => { - if (results.errors.length > 0) { - reject(results.errors[0].message); - } else { - resolve(results.data as ParsedRow[]); + + complete: ({ data, errors }) => { // handle parsing errors + if (errors.length > 0) { + reject(errors[0].message); + return; } + + const drafts: TransactionDraft[] = data.map((raw: unknown) => { // normalize and convert each row to a transaction draft + const normalized = normalizeRow(raw); + return convertDraft(normalized); + }); + + resolve(drafts); // resolve with array of transaction drafts, each containing validation errors if any }, - error: (err) => reject(err.message), + + error: (err) => reject(err.message), // handle file read errors }); }); } From 36a86faaeac52c75fbcf8afaf52314862e79efe7 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 19 Feb 2026 11:39:58 -0600 Subject: [PATCH 067/204] Implemented a preview table and updated the upload folder to maintain state --- .../src/components/csvread/CsvPreview.tsx | 71 +++++++++++++++++++ .../src/components/csvread/CsvUpload.tsx | 49 +++++++++++-- 2 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 app/client/src/components/csvread/CsvPreview.tsx diff --git a/app/client/src/components/csvread/CsvPreview.tsx b/app/client/src/components/csvread/CsvPreview.tsx new file mode 100644 index 0000000..fba2c75 --- /dev/null +++ b/app/client/src/components/csvread/CsvPreview.tsx @@ -0,0 +1,71 @@ +// Component to display parsed CSV rows in a preview table + +import type { TransactionDraft } from "../../util/ConvertTransaction"; + +interface Props { + rows: TransactionDraft[]; +} + +export default function CsvPreviewTable({ rows }: Props) { + // count valid and invalid rows + const validCount = rows.filter(r => r.errors.length === 0).length; + const invalidCount = rows.length - validCount; + + return ( +
+
+ Total rows: {rows.length}
+ Valid rows: {validCount}
+ Invalid rows: {invalidCount} +
+ + + + + + + + + + + + + + + {rows.map((row, index) => { + const isInvalid = row.errors.length > 0; + + return ( + + + + + + + + + ); + })} + +
DateDescriptionAmountSenderRecipientErrors
{row.date}{row.description}{row.amount}{row.sender}{row.recipient} + {isInvalid ? row.errors.join(", ") : "—"} +
+
+ ); +} +const cell: React.CSSProperties = { + border: "1px solid #ccc", + padding: "6px 8px", + fontSize: "0.9rem", +}; diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index 474e14e..84ed7f4 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -1,14 +1,18 @@ // Component to handle file upload, validation, and error display for CSV files //handles one csv file at a time -import React, { useState } from "react"; +import { useState } from "react"; import CsvDrop from "./CsvDrop"; import CsvFile from "./CsvFile"; import CsvError from "./CsvError"; +import CsvPreviewTable from "./CsvPreview"; +import { parseCsvFile } from "../../util/ParseCsv"; +import type { TransactionDraft } from "../../util/ConvertTransaction"; export default function CsvUpload() { const [file, setFile] = useState(null); // selected CSV file const [error, setError] = useState(null); // validation errors - + const [parsedData, setParsedData] = useState(null); // state for parsed and validated data + const [isParsing, setIsParsing] = useState(false); // track if parsing is in progress // File Validation @@ -29,7 +33,7 @@ export default function CsvUpload() { return; } - if (selected.size > 5 * 1024 * 1024) { + if (selected.size > 5 * 1024 * 1024) { // limit file size to 5MB setError("File is too large (max 5MB)."); setFile(null); return; @@ -37,11 +41,37 @@ export default function CsvUpload() { setError(null); setFile(selected); + setParsedData(null); }; const clearFile = () => { setFile(null); setError(null); + setParsedData(null); + + }; + + const handlePreview = async () => { // parses the file and updates state with results + if (!file) return; + + setIsParsing(true); + setError(null); + + try { + const rows = await parseCsvFile(file); // returns array of transaction drafts with validation errors if any + + if (rows.length === 0) { + setError("CSV contains no readable rows."); + setParsedData(null); + } else { + setParsedData(rows); + } + } catch (err: Error | unknown) { // handle parsing errors + setError("Failed to parse CSV: " + (err instanceof Error ? err.message : String(err))); + setParsedData(null); + } + + setIsParsing(false); }; return ( @@ -51,10 +81,17 @@ export default function CsvUpload() { {error && } {file && } - - + + {parsedData && parsedData.length > 0 && ( + + )}
); } From 4de7b74279ea1f377824f70372d1f35e6133b6d5 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Thu, 19 Feb 2026 18:23:10 -0600 Subject: [PATCH 068/204] Visualization: enabled synthetic data for API for expenses chart --- app/client/src/api/ManagerAPI.tsx | 13 ++- app/client/src/pages/DashboardPage.tsx | 90 ++++++++++--------- .../services/ts/api-gateway/src/index.ts | 40 ++++++--- app/server/services/ts/user/src/index.ts | 41 +++++++++ 4 files changed, 127 insertions(+), 57 deletions(-) diff --git a/app/client/src/api/ManagerAPI.tsx b/app/client/src/api/ManagerAPI.tsx index 9a1cfd9..32c0b60 100644 --- a/app/client/src/api/ManagerAPI.tsx +++ b/app/client/src/api/ManagerAPI.tsx @@ -28,12 +28,21 @@ async function getExpensesChartData(period: string): Promise> { if (!["w", "m", "y"].includes(period)) { throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); } - const response = await axios.get(`${BASE_URL}/api/charts/expenses?period=${period}`); + const response = await axios.get(`${BASE_URL}/charts/expenses?period=${period}`); if (response.status !== 200) { throw new Error(`Failed to fetch expenses chart data: ${response.statusText}`); } - return response.data; + return { + labels: response.data["labels"], + datasets: [{ + label: response.data["datasets"][0]["label"], + data: response.data["datasets"][0]["data"], + borderColor: 'rgb(254, 103, 48)', + backgroundColor: 'rgba(255, 47, 47, 0.5)', + }] + }; + } catch (error) { console.error("Error fetching expenses chart data:", error); throw error; diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index f6d31a0..9c1fd04 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -95,45 +95,54 @@ function DashboardPage() { }catch(error) { console.error("Error fetching expenses chart data:", error); } - switch(period) { - case 'w': - return { - labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - datasets: [{ - label: 'Weekly Expenses', - data: [125, 89, 210, 45, 167, 92, 78], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }] - }; + return { + labels: ['Mon'], + datasets: [{ + label: 'Placeholder Expenses', + data: [125], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }] + }; + // switch(period) { + // case 'w': + // return { + // labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + // datasets: [{ + // label: 'Weekly Expenses', + // data: [125, 89, 210, 45, 167, 92, 78], + // borderColor: 'rgb(53, 162, 235)', + // backgroundColor: 'rgba(53, 162, 235, 0.5)', + // }] + // }; - case 'm': - return { - labels: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan', '11 Jan', '12 Jan', - '13 Jan', '14 Jan', '15 Jan', '16 Jan', '17 Jan', '18 Jan', '19 Jan', '20 Jan', '21 Jan', '22 Jan', '23 Jan', '24 Jan', - '25 Jan', '26 Jan', '27 Jan', '28 Jan', '29 Jan', '30 Jan', '31 Jan' - ], - datasets: [{ - label: 'Monthly Expenses', - data: [125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, 65, 190, 120, - 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, 143, 87 - ], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }] - }; + // case 'm': + // return { + // labels: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan', '11 Jan', '12 Jan', + // '13 Jan', '14 Jan', '15 Jan', '16 Jan', '17 Jan', '18 Jan', '19 Jan', '20 Jan', '21 Jan', '22 Jan', '23 Jan', '24 Jan', + // '25 Jan', '26 Jan', '27 Jan', '28 Jan', '29 Jan', '30 Jan', '31 Jan' + // ], + // datasets: [{ + // label: 'Monthly Expenses', + // data: [125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, 65, 190, 120, + // 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, 143, 87 + // ], + // borderColor: 'rgb(53, 162, 235)', + // backgroundColor: 'rgba(53, 162, 235, 0.5)', + // }] + // }; - case 'y': - return { - labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - datasets: [{ - label: 'Yearly Expenses', - data: [3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, 4123], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }] - }; - } + // case 'y': + // return { + // labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + // datasets: [{ + // label: 'Yearly Expenses', + // data: [3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, 4123], + // borderColor: 'rgb(53, 162, 235)', + // backgroundColor: 'rgba(53, 162, 235, 0.5)', + // }] + // }; + // } }; const expensesBarOptions = { @@ -229,10 +238,9 @@ const incomeSankeyOptions = { }; return ( -
- {/*p-10 bg-gray-100 */} -

Welcome Username

-

Here you can view your recent transactions and manage your finances.

+
+

Welcome Username

+

Here you can view your recent transactions and manage your finances.

diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 6b8c70f..b52c026 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -25,17 +25,29 @@ onExit(() => server.close()); //API gateway sits on port 3000 and is accessible from there. Go to browser and type http://localhost:3000/health and you should see which services are up. - app.get('/health', async (req: express.Request, res: express.Response) => { - const result: { [string]: string} = {}; - const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); - - for(const service of services) { - const serviceName = service.split('_')[0]; - result[serviceName] = await fetch(`${process.env[service]}/health`) - .then((res) => res.text()) - .catch((err) => err.message); - console.log("received response"); - } - - res.json(result); - }); +app.get('/health', async (req: express.Request, res: express.Response) => { + const result: { [string]: string} = {}; + const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); + + for(const service of services) { + const serviceName = service.split('_')[0]; + result[serviceName] = await fetch(`${process.env[service]}/health`) + .then((res) => res.text()) + .catch((err) => err.message); + console.log("received response"); + } + + res.json(result); +}); + +app.get('/charts/expenses', async (req: express.Request, res: express.Response) => { + console.log("Received request for expenses chart data with query:", req.query); + const period = req.query.period as string; + const response = await fetch(`${process.env.USER_SERVICE_ADDR}/charts/expenses?period=${period}`) + .then((res) => res.json()) + .catch((err) => { + console.error("Error fetching expenses chart data:", err); + res.status(500).json({ error: "Failed to fetch expenses chart data" }); + }); + res.json(response); +}); diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index a783fd1..3f39e04 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -16,3 +16,44 @@ onExit(() => server.close()); app.get('/health', (req: express.Request, res: express.Response) => { res.send('ok'); }); + +app.get('/charts/expenses', (req: express.Request, res: express.Response) => { + const period = req.query.period as string; + if (!["w", "m", "y"].includes(period)) { + res.status(400).json({ error: "Invalid period. Must be 'w', 'm', or 'y'." }); + return; + } + switch(period) { + case 'w': + return res.json({ + labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + datasets: [{ + label: 'Weekly Expenses', + data: [125, 89, 210, 45, 167, 92, 78] + }] + }); + + case 'm': + return res.json({ + labels: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan', '11 Jan', '12 Jan', + '13 Jan', '14 Jan', '15 Jan', '16 Jan', '17 Jan', '18 Jan', '19 Jan', '20 Jan', '21 Jan', '22 Jan', '23 Jan', '24 Jan', + '25 Jan', '26 Jan', '27 Jan', '28 Jan', '29 Jan', '30 Jan', '31 Jan' + ], + datasets: [{ + label: 'Monthly Expenses', + data: [125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, 65, 190, 120, + 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, 143, 87 + ] + }] + }); + + case 'y': + return res.json({ + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + datasets: [{ + label: 'Yearly Expenses', + data: [3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, 4123] + }] + }); + } +}); From 655019bf7a1f9d87eca71542c451da40525712e7 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 19 Feb 2026 19:20:41 -0600 Subject: [PATCH 069/204] Updated files to import types --- .../services/database/src/models/FinancialAccount.ts | 2 +- app/server/services/database/src/models/Profile.ts | 2 +- app/server/services/database/src/models/ProfileLink.ts | 2 +- app/server/services/database/src/models/Transaction.ts | 2 +- app/server/services/database/src/routes/account.ts | 7 +++++-- app/server/services/database/src/routes/profile.ts | 8 +++++--- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/server/services/database/src/models/FinancialAccount.ts b/app/server/services/database/src/models/FinancialAccount.ts index 5f9f862..678cb2c 100644 --- a/app/server/services/database/src/models/FinancialAccount.ts +++ b/app/server/services/database/src/models/FinancialAccount.ts @@ -1,4 +1,4 @@ -import { RowDataPacket } from "mysql2"; +import type { RowDataPacket } from "mysql2"; // shows account types (chequing, savings, or investment accounts) export interface FinancialAccount extends RowDataPacket { id: number; diff --git a/app/server/services/database/src/models/Profile.ts b/app/server/services/database/src/models/Profile.ts index 77fb30a..5a653c3 100644 --- a/app/server/services/database/src/models/Profile.ts +++ b/app/server/services/database/src/models/Profile.ts @@ -1,4 +1,4 @@ -import { RowDataPacket } from "mysql2"; +import type { RowDataPacket } from "mysql2"; // users view of accounts and goals export interface Profile extends RowDataPacket { id: number; diff --git a/app/server/services/database/src/models/ProfileLink.ts b/app/server/services/database/src/models/ProfileLink.ts index 0a792d6..279a6b5 100644 --- a/app/server/services/database/src/models/ProfileLink.ts +++ b/app/server/services/database/src/models/ProfileLink.ts @@ -1,4 +1,4 @@ -import { RowDataPacket } from "mysql2"; +import type { RowDataPacket } from "mysql2"; //shows relationship betwen user and their profiles/accounts export interface UserProfileLink extends RowDataPacket{ profile_id: number; diff --git a/app/server/services/database/src/models/Transaction.ts b/app/server/services/database/src/models/Transaction.ts index 126c611..8b5838b 100644 --- a/app/server/services/database/src/models/Transaction.ts +++ b/app/server/services/database/src/models/Transaction.ts @@ -1,4 +1,4 @@ -import { RowDataPacket } from "mysql2"; +import type { RowDataPacket } from "mysql2"; //represents individual transaction events (spending, income, transfers) export interface Transaction extends RowDataPacket{ id: number; diff --git a/app/server/services/database/src/routes/account.ts b/app/server/services/database/src/routes/account.ts index 77caaa2..0f0aaa5 100644 --- a/app/server/services/database/src/routes/account.ts +++ b/app/server/services/database/src/routes/account.ts @@ -1,7 +1,9 @@ // account creation route to db service //TODO import db connection -import { Router, Request, Response } from "express"; +import { Router } from "express"; +import type { Request, Response } from "express"; +import type { ResultSetHeader } from "mysql2"; import { db } from "../db/connection"; export const accountsRouter = Router(); @@ -10,7 +12,7 @@ accountsRouter.post("/", async (req: Request, res: Response) => { try { const { name, type, balance, value, subtype } = req.body; - const [result] = await db.query( + const [result] = await db.query( `INSERT INTO financialAccount (name, type, balance, value, subtype) VALUES (?, ?, ?, ?, ?)`, [name, type, balance, value, subtype ?? null] @@ -22,5 +24,6 @@ accountsRouter.post("/", async (req: Request, res: Response) => { }); } catch (err) { console.error("Account creation failed", err); + res.status(500).json({ error: "Account creation failed" }); } }); diff --git a/app/server/services/database/src/routes/profile.ts b/app/server/services/database/src/routes/profile.ts index 6c4cce4..df3060a 100644 --- a/app/server/services/database/src/routes/profile.ts +++ b/app/server/services/database/src/routes/profile.ts @@ -1,7 +1,8 @@ // profile creation route this lets us create profiles in the db , which can be used to group accounts together and track them. - -import { Router, Request, Response } from "express"; +import { Router } from "express"; +import type { Request, Response } from "express"; +import type { ResultSetHeader } from "mysql2"; import { db } from "../db/connection"; export const profilesRouter = Router(); @@ -11,7 +12,7 @@ profilesRouter.post("/", async (req: Request, res: Response) => { try { const { name, description } = req.body; - const [result] = await db.query( + const [result] = await db.query( `INSERT INTO profile (name, description) VALUES (?, ?)`, [name, description ?? null] @@ -23,5 +24,6 @@ profilesRouter.post("/", async (req: Request, res: Response) => { }); } catch (err) { console.error("Profile creation failed:", err); + res.status(500).json({ error: "Profile creation failed" }); } }); From d54b5e1e65e3b97f77995ab6a3732959f84b5d17 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 19 Feb 2026 19:21:54 -0600 Subject: [PATCH 070/204] Added confirmation to the csv upload. Also updated Csv Upload to implement this behavior --- .../src/components/csvread/CsvConfirm.tsx | 44 +++++++++++ .../src/components/csvread/CsvUpload.tsx | 79 ++++++++++++++++--- 2 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 app/client/src/components/csvread/CsvConfirm.tsx diff --git a/app/client/src/components/csvread/CsvConfirm.tsx b/app/client/src/components/csvread/CsvConfirm.tsx new file mode 100644 index 0000000..fc158f8 --- /dev/null +++ b/app/client/src/components/csvread/CsvConfirm.tsx @@ -0,0 +1,44 @@ +//shows a summary of parsed CSV data before final import + +import type { TransactionDraft } from "../../util/ConvertTransaction"; + +interface Props { + rows: TransactionDraft[]; + onConfirm: () => void; + onBack: () => void; +} + +export default function CsvConfirmation({ rows, onConfirm, onBack }: Props) { // calculates summary stats and displays them along with confirm/back buttons + const validRows = rows.filter(r => r.errors.length === 0); + const invalidRows = rows.filter(r => r.errors.length > 0); + + const totalIncome = validRows + .filter(r => (r.amount ?? 0) > 0) + .reduce((sum, r) => sum + (r.amount ?? 0), 0); + + const totalExpenses = validRows + .filter(r => (r.amount ?? 0) < 0) + .reduce((sum, r) => sum + (r.amount ?? 0), 0); + + return ( +
+

Summary

+ +
+ Total rows: {rows.length}
+ Valid rows: {validRows.length}
+ Invalid rows: {invalidRows.length}
+ Total income: ${totalIncome.toFixed(2)}
+ Total expenses: ${totalExpenses.toFixed(2)} +
+ + + + +
+ ); +} diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index 84ed7f4..5fa55f7 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -5,6 +5,7 @@ import CsvDrop from "./CsvDrop"; import CsvFile from "./CsvFile"; import CsvError from "./CsvError"; import CsvPreviewTable from "./CsvPreview"; +import CsvConfirmation from "./CsvConfirm"; import { parseCsvFile } from "../../util/ParseCsv"; import type { TransactionDraft } from "../../util/ConvertTransaction"; @@ -13,6 +14,8 @@ export default function CsvUpload() { const [error, setError] = useState(null); // validation errors const [parsedData, setParsedData] = useState(null); // state for parsed and validated data const [isParsing, setIsParsing] = useState(false); // track if parsing is in progress + const [showConfirmation, setShowConfirmation] = useState(false); + // File Validation @@ -42,13 +45,14 @@ export default function CsvUpload() { setError(null); setFile(selected); setParsedData(null); + setShowConfirmation(false); }; const clearFile = () => { setFile(null); setError(null); setParsedData(null); - + setShowConfirmation(false); }; const handlePreview = async () => { // parses the file and updates state with results @@ -74,24 +78,77 @@ export default function CsvUpload() { setIsParsing(false); }; +//handle import sends valid transactions to backend for insertion into database +const handleImport = async () => { + if (!parsedData) return; + + try { + const response = await fetch("/api/transactions/csvTransaction", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + financialAccount_id: 1, // TODO: replace with real account ID + transactions: parsedData + }), + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || "Import failed"); + return; + } + + console.log("Import success:", result); + // TODO: show success UI or redirect + } catch (err) { + setError("Network error during import"); + } +}; + + + return ( -
- +
+ + + {error && } - {error && } + {file && } - {file && } + {/* only show preview button if we have a file and we're not already in confirmation step */} + {!showConfirmation && ( + )} - {parsedData && parsedData.length > 0 && ( + {/* show preview table and continue button if we have parsed data and we're not in confirmation step */} + {parsedData && parsedData.length > 0 && !showConfirmation && ( + <> - )} -
- ); -} + + + + )} + + {/* show confirmation step */} + {parsedData && parsedData.length > 0 && showConfirmation && ( + setShowConfirmation(false)} + onConfirm={handleImport} // send to backend + /> + )} +
+); +} \ No newline at end of file From 1677acb788abed04b3f9742c6290a61078b0680b Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Thu, 19 Feb 2026 19:22:42 -0600 Subject: [PATCH 071/204] Implemented transaction backend route for batch csv data upload --- .../database/src/routes/transaction.ts | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/app/server/services/database/src/routes/transaction.ts b/app/server/services/database/src/routes/transaction.ts index f876532..c03fe80 100644 --- a/app/server/services/database/src/routes/transaction.ts +++ b/app/server/services/database/src/routes/transaction.ts @@ -1,5 +1,7 @@ // transaction routes for creating and updating transactions -import { Router, Request, Response } from "express"; +import { Router } from "express"; +import type { Request, Response } from "express"; +import type { ResultSetHeader } from "mysql2"; import { db } from "../db/connection"; export const transactionsRouter = Router(); @@ -10,7 +12,7 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { try { const { financialAccount_id, amount, description, sender, recipient, date } = req.body; - const [result] = await db.query( + const [result] = await db.query( `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) VALUES (?, ?, ?, ?, ?, ?)`, [financialAccount_id, amount, description ?? null, sender ?? null, recipient ?? null, date] @@ -22,6 +24,7 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { }); } catch (err) { console.error("Transaction creation failed", err); + res.status(500).json({ error: "Transaction creation failed" }); } }); @@ -44,3 +47,61 @@ transactionsRouter.put("/:id", async (req: Request, res: Response) => { } catch (err) { console.error("Transaction update failed", err); } }); + +// CSV Transaction +transactionsRouter.post("/csvTransaction", async (req: Request, res: Response) => { + try { + const { financialAccount_id, transactions } = req.body; + + if (!financialAccount_id || !Array.isArray(transactions)) { + return res.status(400).json({ error: "Invalid request payload" }); + } + + const validRows = transactions.filter((t: any) => t.errors.length === 0); + + if (validRows.length === 0) { + return res.status(400).json({ error: "No valid transactions to import" }); + } + + const connection = await db.getConnection(); + + try { + await connection.beginTransaction(); + + for (const row of validRows) { + await connection.query( + `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + financialAccount_id, + row.amount, + row.description ?? null, + row.sender ?? null, + row.recipient ?? null, + row.date, + ] + ); + } + + await connection.commit(); + + res.json({ + message: "CSV transactions imported successfully", + inserted: validRows.length, + skipped: transactions.length - validRows.length, + }); + + } catch (err) { + await connection.rollback(); + console.error("CSV transaction import failed", err); + res.status(500).json({ error: "CSV transaction import failed" }); + + } finally { + connection.release(); + } + + } catch (err) { + console.error("CSV transaction import failed", err); + res.status(500).json({ error: "CSV transaction import failed" }); + } +}); From 64252d9e46b5f1b5ea2338140b11499ec3a010b7 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Thu, 19 Feb 2026 22:04:08 -0600 Subject: [PATCH 072/204] Visualization: added savings and income charts with synthetic data --- app/client/bun.lock | 77 +++++++++ app/client/package.json | 1 + app/client/src/api/ManagerAPI.tsx | 94 +++++++++-- app/client/src/components/SankeyChart.tsx | 71 +++++++++ app/client/src/pages/DashboardPage.tsx | 186 +++++++++++++--------- 5 files changed, 337 insertions(+), 92 deletions(-) create mode 100644 app/client/src/components/SankeyChart.tsx diff --git a/app/client/bun.lock b/app/client/bun.lock index 9f8aefd..5561d85 100644 --- a/app/client/bun.lock +++ b/app/client/bun.lock @@ -18,6 +18,7 @@ "react-dom": "^19.2.0", "react-icons": "^5.5.0", "react-router-dom": "^7.13.0", + "recharts": "^3.7.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", }, @@ -252,6 +253,8 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="], @@ -282,6 +285,10 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@swc/core": ["@swc/core@1.15.11", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.11", "@swc/core-darwin-x64": "1.15.11", "@swc/core-linux-arm-gnueabihf": "1.15.11", "@swc/core-linux-arm64-gnu": "1.15.11", "@swc/core-linux-arm64-musl": "1.15.11", "@swc/core-linux-x64-gnu": "1.15.11", "@swc/core-linux-x64-musl": "1.15.11", "@swc/core-win32-arm64-msvc": "1.15.11", "@swc/core-win32-ia32-msvc": "1.15.11", "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg=="], @@ -340,6 +347,24 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/history": ["@types/history@4.7.11", "", {}, "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="], @@ -356,6 +381,8 @@ "@types/react-router-dom": ["@types/react-router-dom@5.3.3", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "*" } }, "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="], @@ -432,8 +459,32 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], @@ -456,6 +507,8 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -480,6 +533,8 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -534,10 +589,14 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -648,6 +707,10 @@ "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], + "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], + + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -658,6 +721,14 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="], @@ -684,6 +755,8 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], @@ -710,6 +783,8 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "vite": ["rolldown-vite@7.2.5", "", { "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.50", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -728,6 +803,8 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], diff --git a/app/client/package.json b/app/client/package.json index f182384..4803a56 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -23,6 +23,7 @@ "react-dom": "^19.2.0", "react-icons": "^5.5.0", "react-router-dom": "^7.13.0", + "recharts": "^3.7.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, diff --git a/app/client/src/api/ManagerAPI.tsx b/app/client/src/api/ManagerAPI.tsx index 32c0b60..1976d3c 100644 --- a/app/client/src/api/ManagerAPI.tsx +++ b/app/client/src/api/ManagerAPI.tsx @@ -1,6 +1,7 @@ import axios from "axios"; import { BASE_URL } from "@/utils/constants"; import type { ChartData } from "chart.js"; +import type { SankeyData } from 'recharts/types/chart/Sankey'; import type React from "react"; // import type { Transaction } from "@/types/Transaction"; @@ -51,34 +52,95 @@ async function getExpensesChartData(period: string): Promise> { // Savings contribution chart -async function getSavingsContribChartData(period: string): Promise { +async function getSavingsContribChartData(period: string): Promise> { try { if (!["w", "m", "y"].includes(period)) { throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); } - const response = await axios.get(`${BASE_URL}/api/charts/savings-contrib?period=${period}`); - if (response.status !== 200) { - throw new Error(`Failed to fetch savings contribution chart data: ${response.statusText}`); - } - return response.data; - } catch (error) { - console.error("Error fetching savings contribution chart data:", error); - throw error; - }; + // const response = await axios.get(`${BASE_URL}/charts/savings-contrib?period=${period}`); + // if (response.status !== 200) { + // throw new Error(`Failed to fetch savings contribution chart data: ${response.statusText}`); + // } + //return response.data; + switch(period) { + case 'w': + return { + labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + datasets: [{ + label: 'Weekly Savings ', + data: [125, 300, 350, 225, 600, 900, 1200], + backgroundColor: 'rgba(68, 255, 21, 0.5)', + borderColor: 'rgb(68, 255, 21)', + }] + }; + + case 'm': + return { + labels: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan', '11 Jan', '12 Jan', + '13 Jan', '14 Jan', '15 Jan', '16 Jan', '17 Jan', '18 Jan', '19 Jan', '20 Jan', '21 Jan', '22 Jan', '23 Jan', '24 Jan', + '25 Jan', '26 Jan', '27 Jan', '28 Jan', '29 Jan', '30 Jan', '31 Jan' + ], + datasets: [{ + label: 'Monthly Savings', + data: [125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, 65, 190, 120, + 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, 143, 87 + ], + backgroundColor: 'rgba(68, 255, 21, 0.5)', + borderColor: 'rgb(68, 255, 21)', + }] + }; + + case 'y': + return { + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + datasets: [{ + label: 'Yearly Savings', + data: [3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, 4123], + backgroundColor: 'rgba(68, 255, 21, 0.5)', + borderColor: 'rgb(68, 255, 21)', + }] + }; + + default: + return { + labels: [], + datasets: [] + } + } + } catch (error) { + console.error("Error fetching savings contribution chart data:", error); + throw error; + }; } // Income flow - sankey chart data -async function getIncomeFlowChartData(period: string): Promise { +async function getIncomeFlowChartData(period: string): Promise { try { if (!["w", "m", "y"].includes(period)) { throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); } - const response = await axios.get(`${BASE_URL}/api/charts/income-flow?period=${period}`); - if (response.status !== 200) { - throw new Error(`Failed to fetch income flow chart data: ${response.statusText}`); - } - return response.data; + // const response = await axios.get(`${BASE_URL}/charts/income-flow?period=${period}`); + // if (response.status !== 200) { + // throw new Error(`Failed to fetch income flow chart data: ${response.statusText}`); + // } + //return response.data; + const dataTest = { + nodes: [ + { name: 'Salary' }, + { name: 'Pokemon Cards' }, + { name: 'Total Income' }, + { name: 'Food' }, + { name: 'Mortgage' }, + ], + links: [ + { source: 0, target: 1, value: 3728.3 }, + { source: 0, target: 2, value: 354170 }, + { source: 2, target: 3, value: 62429 }, + { source: 2, target: 4, value: 291741 }, + ], + }; + return dataTest; } catch (error) { console.error("Error fetching income flow chart data:", error); throw error; diff --git a/app/client/src/components/SankeyChart.tsx b/app/client/src/components/SankeyChart.tsx new file mode 100644 index 0000000..045fd0a --- /dev/null +++ b/app/client/src/components/SankeyChart.tsx @@ -0,0 +1,71 @@ +import { ResponsiveContainer, Sankey, Tooltip, useChartWidth, Layer, Rectangle, type SankeyNodeProps } from 'recharts'; + +const TEXT_OFFSET = 6; + +interface SankeyData { + nodes: Array<{ name: string }>;//names of nodes + links: Array<{//links between nodes - source and target are indices of the nodes in the nodes array, value is the weight of the link + source: number; + target: number; + value: number; + }>; +} + +interface SankeyChartProps { + data: SankeyData; +} + +export function SankeyChart({data}: SankeyChartProps) { + return ( + + + + + ); +} + + +function CustomNode({ x, y, width, height, index, payload }: SankeyNodeProps) { + const containerWidth = useChartWidth(); + if (containerWidth == null) { + return null; // return null if used outside a chart context + } + console.log("width of container:", containerWidth) + console.log("x:", x, "width of node:", width); + const isOut = x + width + TEXT_OFFSET > containerWidth-TEXT_OFFSET*3; + return ( + + + + {payload.name} + + + {`${payload.value}k`} + + + ); +} + +export default SankeyChart; \ No newline at end of file diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 9c1fd04..6dff1e6 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -4,9 +4,11 @@ import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScal import { Pie, Line, Bar } from 'react-chartjs-2'; import AccountCard from '@/components/AccountCard'; import TransactionTable from '@/components/TransactionTable'; -import {Button} from '@/components/button'; +import {Button} from '@/components/Button'; import LoadingSpinner from '@/components/LoadingSpinner'; +import SankeyChart from '@/components/SankeyChart'; import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; +import type { SankeyData } from 'recharts/types/chart/Sankey'; Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); @@ -20,6 +22,8 @@ function DashboardPage() { //Chart data state - this is used to determine whether to put a spinner in place of a chart while data is being fetched from API const [expensesData, setExpensesData] = useState | null>(null); + const [savingsData, setsavingsData] = useState | null>(null); + const [incomeData, setIncomeData] = useState(null); const [isLoading, setIsLoading] = useState(false); const data = { @@ -67,22 +71,59 @@ function DashboardPage() { }, }; - useEffect(() => { - const fetchExpensesData = async () => { - if (activeChart !== 'expenses') return; - - setIsLoading(true); - try { - const data = await getTestExpensesData(selectedPeriod); - setExpensesData(data); - } catch (error) { - console.error("Failed to fetch expenses data:", error); - } finally { - setIsLoading(false); - } - }; - fetchExpensesData(); + const fetchExpensesData = async () => { + setIsLoading(true); + try { + const data = await getTestExpensesData(selectedPeriod); + setExpensesData(data); + } catch (error) { + console.error("Failed to fetch expenses data:", error); + } finally { + setIsLoading(false); + } + }; + + const fetchSavingsData = async () => { + setIsLoading(true); + try { + const data = await getTestSavingsContribData(selectedPeriod); + setsavingsData(data); + } catch (error) { + console.error("Failed to fetch savings data:", error); + } finally { + setIsLoading(false); + } + }; + + const fetchIncomeData = async () => { + setIsLoading(true); + try { + const data = await getTestIncomeFlowData(selectedPeriod); + //console.log("Fetched income flow data:", data); + setIncomeData(data); + } catch (error) { + console.error("Failed to fetch income data:", error); + } finally { + setIsLoading(false); + } + }; + + + useEffect(() => { + switch(activeChart) { + case 'expenses': + fetchExpensesData(); + break; + case 'savings': + fetchSavingsData(); + break; + case 'income': + fetchIncomeData(); + break; + default: + fetchExpensesData(); + } }, [activeChart, selectedPeriod]); @@ -104,65 +145,44 @@ function DashboardPage() { backgroundColor: 'rgba(53, 162, 235, 0.5)', }] }; - // switch(period) { - // case 'w': - // return { - // labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - // datasets: [{ - // label: 'Weekly Expenses', - // data: [125, 89, 210, 45, 167, 92, 78], - // borderColor: 'rgb(53, 162, 235)', - // backgroundColor: 'rgba(53, 162, 235, 0.5)', - // }] - // }; - - // case 'm': - // return { - // labels: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan', '11 Jan', '12 Jan', - // '13 Jan', '14 Jan', '15 Jan', '16 Jan', '17 Jan', '18 Jan', '19 Jan', '20 Jan', '21 Jan', '22 Jan', '23 Jan', '24 Jan', - // '25 Jan', '26 Jan', '27 Jan', '28 Jan', '29 Jan', '30 Jan', '31 Jan' - // ], - // datasets: [{ - // label: 'Monthly Expenses', - // data: [125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, 65, 190, 120, - // 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, 143, 87 - // ], - // borderColor: 'rgb(53, 162, 235)', - // backgroundColor: 'rgba(53, 162, 235, 0.5)', - // }] - // }; - - // case 'y': - // return { - // labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - // datasets: [{ - // label: 'Yearly Expenses', - // data: [3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, 4123], - // borderColor: 'rgb(53, 162, 235)', - // backgroundColor: 'rgba(53, 162, 235, 0.5)', - // }] - // }; - // } }; -const expensesBarOptions = { - responsive: true, - plugins: { - legend: { - position: 'top' as const, - }, - title: { - display: true, - text: 'Expenses Over Time', - font: { - size: 24, - weight: 'bold' as const - } + + const getTestSavingsContribData = async (period: 'w' | 'm' | 'y'): Promise> => { + try{ + const response = await getSavingsContribChartData(period); + return response; + }catch(error) { + console.error("Error fetching savings contribution chart data:", error); } - }, -}; + return { + labels: ['Mon'], + datasets: [{ + label: 'Placeholder Savings Contribution', + data: [125], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }] + }; + } -const savingsLineOptions = { + const getTestIncomeFlowData = async (period: 'w' | 'm' | 'y'): Promise => { + try{ + const response = await getIncomeFlowChartData(period); + //console.log("Received income flow chart data:", response); + return response; + }catch(error) { + console.error("Error fetching income flow chart data:", error); + } + console.log("Using placeholder income flow chart data"); + return { + nodes: [], + links: [], + }; + } + + +const expensesBarOptions = { responsive: true, plugins: { legend: { @@ -179,7 +199,7 @@ const savingsLineOptions = { }, }; -const incomeSankeyOptions = { +const savingsLineOptions = { responsive: true, plugins: { legend: { @@ -214,17 +234,29 @@ const incomeSankeyOptions = {
); case 'savings': + if (isLoading || !savingsData) { + return ( +
+ < LoadingSpinner /> +
+ ); + } return (
- {/* savingsLineOptions */} -

Savings Chart (Coming Soon)

+
); case 'income': + if (isLoading || !incomeData) { + return ( +
+ < LoadingSpinner /> +
+ ); + } return (
- -

Income Flow Chart (Coming Soon)

+
); default: @@ -237,6 +269,8 @@ const incomeSankeyOptions = { } }; + + return (

Welcome Username

From 6ba716ed1c989ecede0639f8d9f5407fcf36c178 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Thu, 19 Feb 2026 22:29:05 -0600 Subject: [PATCH 073/204] Visualization: updated button component to use default react code --- app/client/src/components/Button.tsx | 64 -------------------------- app/client/src/pages/DashboardPage.tsx | 14 +++--- 2 files changed, 7 insertions(+), 71 deletions(-) delete mode 100644 app/client/src/components/Button.tsx diff --git a/app/client/src/components/Button.tsx b/app/client/src/components/Button.tsx deleted file mode 100644 index b5ea4ab..0000000 --- a/app/client/src/components/Button.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant = "default", - size = "default", - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot.Root : "button" - - return ( - - ) -} - -export { Button, buttonVariants } diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 6dff1e6..071bc58 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -4,7 +4,7 @@ import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScal import { Pie, Line, Bar } from 'react-chartjs-2'; import AccountCard from '@/components/AccountCard'; import TransactionTable from '@/components/TransactionTable'; -import {Button} from '@/components/Button'; +//import {Button} from '@/components/Button'; import LoadingSpinner from '@/components/LoadingSpinner'; import SankeyChart from '@/components/SankeyChart'; import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; @@ -283,16 +283,16 @@ const savingsLineOptions = {
- - - + + +
{/* This chart is here just to test all the graph components */} - - - + + + {renderChart()}
From e75a3ec95fe9618c222333ae2159b851b2ccdacb Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 20 Feb 2026 04:44:33 -0600 Subject: [PATCH 074/204] Styled the form components and constricted user input in forms. --- app/client/src/App.tsx | 2 +- app/client/src/components/ui/AccountForm.tsx | 104 +++++++++++------- .../src/components/ui/TransationForm.tsx | 68 ++++++++---- app/client/src/components/ui/userForm.css | 67 +++++++++++ 4 files changed, 184 insertions(+), 57 deletions(-) create mode 100644 app/client/src/components/ui/userForm.css diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index ed9a0cf..7d2cc10 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -71,7 +71,7 @@ function NotFoundPage() { {seen ? ():null} - {seen ? ():null} + ) } diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index 58deb6b..48288d7 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import CsvUpload from "../csvread/CsvUpload"; +import './userForm.css' interface popupProp{ toggle: () => void; @@ -13,13 +14,9 @@ const accountCategory = { DEBT: "Debt" } -export default function popupForm({toggle, edit}:popupProp,){ - - //Add whne getUserAccounts is implemented - //const userAccounts = getUserAccounts() +type typeofAccount = keyof typeof accountCategory - const [formInput, setFormInput] = useState({name: '', balence: 0, type:'', subType:'', }) - const [file, setFile] = useState({statement: null}) +export default function popupForm({toggle, edit}:popupProp,){ //Submits the account data const handleSubmit = () => { @@ -31,65 +28,98 @@ export default function popupForm({toggle, edit}:popupProp,){ setFormInput({...formInput, [event.target.name] : event.target.value}); } + //Handles state when currency is changed + const handleCurrencyChange = (event:React.ChangeEvent) => { + + let input = event.target.value + const pattern = /^\d*\.?\d{0,2}$/ + + console.log(input) + console.log(pattern.test(input)) + //Determine if the input follows the format/pattern + if(pattern.test(input) || input === ""){ + input = input.replace(/^0+(?=\d)/,"") + setBalence(input) + + } + } + //Handles the bank statement const handleFile = (event:React.ChangeEvent) => { + if(event.target && event.target.files && event.target.files[0]){ + setFile(event.target.files[0]) + } else { + setFile(undefined) + } } + + //Add when getUserAccounts is implemented + //const userAccounts = getUserAccounts() + + const [formInput, setFormInput] = useState({name: '', type:'', subType:''}) + const [file, setFile] = useState(undefined) + const[balence, setBalence] = useState("") + const[accountType, setAccountType] = useState(undefined) + + const accountCat: typeofAccount[] = Object.keys(accountCategory) as typeofAccount[] + return( <> -
+
- {edit ? (

Edit Account

):(

Create Account

)} + handleSubmit() }> - handleSubmit()}> + {edit ? (

Edit Account

):(

Create Account

)} - +

- - setAccountType(event.target.value as typeofAccount)}> - - - - + {accountCat.map(category => ())}

- - + +



- {(formInput.type == "debt" || formInput.type == "saving") ? ( - <> - - -

- + + {(accountType === "DEBT" || accountType === "SAVING") ? ( + <> + + +

+ ): null } - {(formInput.type == "saving") ? ( - <> - - -

- + {(accountType === "SAVING") ? ( + <> + + +

+ ):null } - - + +
+ + +
diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index e342bc6..d8e4148 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import React, { useState, type ReactHTMLElement } from "react"; import CsvUpload from "../csvread/CsvUpload"; +import './userForm.css' interface popupProp{ toggle: () => void; @@ -24,55 +25,84 @@ export default function popupForm({toggle, edit}:popupProp){ //Handles the submiting the form const handleSubmit = () =>{ toggle() - //Determine if the input is valid - if(selectedType && amount != 0) { - //Insert function that handles api communication between client and server + + } + + //Handles state when currency is changed + const handleCurrencyChange = (event:React.ChangeEvent) => { + + let input = event.target.value + const pattern = /^\d*\.?\d{0,2}$/ + + console.log(input) + console.log(pattern.test(input)) + //Determine if the input follows the format/pattern + if(pattern.test(input) || input === ""){ + input = input.replace(/^0+(?=\d)/,"") + setAmount(input) + } } - const handleFile = () =>{ + const handleCurrencyBlur = (event:React.ChangeEvent) => { + if(event.target.value !== "") { + setAmount(parseFloat(amount).toFixed(2)) + } + + } + + const handleFile = (event:React.ChangeEvent) =>{ + + if(event.target && event.target.files && event.target.files[0]){ + setFile(event.target.files[0]) + } else { + setFile(undefined) + } + + //Insert cvs parsing function } //Holds state of user input const [selectedType, setSelectedType] = useState(undefined) - const [amount, setAmount] = useState(0) - const [file, setFile] = useState(null) + const [amount, setAmount] = useState("") + const [file, setFile] = useState(undefined) //Holds the types of transfers const transCat: typeOfTransfers [] = Object.keys(transferCategory) as typeOfTransfers[]; return( <> -
- {edit ? (

Edit Transaction

):(

Create Transaction

)} +
- - + {edit ? (

Edit Transaction

):(

Create Transaction

)} + +

- +

- - setAmount(Number(event.target.value))}/> + +



- - - {edit ?():()} +
+ + {edit ?():()} +
diff --git a/app/client/src/components/ui/userForm.css b/app/client/src/components/ui/userForm.css new file mode 100644 index 0000000..9f35bd4 --- /dev/null +++ b/app/client/src/components/ui/userForm.css @@ -0,0 +1,67 @@ + +form { + background-color: lightgrey; + padding: 15px; + place-content: start; + height: auto; +} + +input{ + margin-top: 2.5%; + margin-bottom: 2.5%; + margin-left: 1%; + +} + +select{ + margin-top: 2.5%; + margin-bottom: 2.5%; + margin-left: 1%; +} + +.popup{ + color:black; + background-color:floralwhite; + padding: 15px; + border-radius: 5px; + border-style: solid; + border-width: 2px; + border-color: black; + + + position:fixed; + top:0; + bottom: 0; + right: 35%; + left: 35%; + z-index: 100; + width: max-content; + height: max-content; + + font-size: 10pt; + + + justify-content: center; + align-items: center; +} + +button{ + padding: 5px; + border-width:1px; + border-color: black; + border-radius: 5px; +} + +.bottomButtons{ + display: flex; +} + +.bottomButtons :first-child{ + margin-right: auto; + background-color: lightcoral; +} +.bottomButtons button:last-child{ + margin-left: auto; + background-color: aquamarine; +} + From 6795ebb300ccb6be56a40ab78d1b0d9b2495520f Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 20 Feb 2026 08:32:49 -0600 Subject: [PATCH 075/204] re-arranged ts service dockerfiles to better utilize layer caching --- app/server/services/ts/Dockerfile | 18 ++++++++++++++++++ app/server/services/ts/api-gateway/Dockerfile | 11 +++++++---- app/server/services/ts/auth/Dockerfile | 10 +++++++--- app/server/services/ts/user/Dockerfile | 12 ++++++++---- 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 app/server/services/ts/Dockerfile diff --git a/app/server/services/ts/Dockerfile b/app/server/services/ts/Dockerfile new file mode 100644 index 0000000..64cf526 --- /dev/null +++ b/app/server/services/ts/Dockerfile @@ -0,0 +1,18 @@ +FROM oven/bun:1.0-slim + +WORKDIR /project +COPY package.json . +RUN bun install + +WORKDIR /project/app +COPY auth/package.json . +RUN bun install + +WORKDIR /project +COPY tsconfig.json . +COPY common ./common + +WORKDIR /project/app +COPY auth/src ./src + +CMD ["bun", "src/index.ts"] diff --git a/app/server/services/ts/api-gateway/Dockerfile b/app/server/services/ts/api-gateway/Dockerfile index ca0a1f3..64cf526 100644 --- a/app/server/services/ts/api-gateway/Dockerfile +++ b/app/server/services/ts/api-gateway/Dockerfile @@ -2,14 +2,17 @@ FROM oven/bun:1.0-slim WORKDIR /project COPY package.json . -COPY tsconfig.json . -COPY common ./common RUN bun install WORKDIR /project/app -COPY api-gateway/package.json . -COPY api-gateway/src ./src +COPY auth/package.json . RUN bun install +WORKDIR /project +COPY tsconfig.json . +COPY common ./common + +WORKDIR /project/app +COPY auth/src ./src CMD ["bun", "src/index.ts"] diff --git a/app/server/services/ts/auth/Dockerfile b/app/server/services/ts/auth/Dockerfile index bc99e3e..64cf526 100644 --- a/app/server/services/ts/auth/Dockerfile +++ b/app/server/services/ts/auth/Dockerfile @@ -2,13 +2,17 @@ FROM oven/bun:1.0-slim WORKDIR /project COPY package.json . -COPY tsconfig.json . -COPY common ./common RUN bun install WORKDIR /project/app COPY auth/package.json . -COPY auth/src src RUN bun install +WORKDIR /project +COPY tsconfig.json . +COPY common ./common + +WORKDIR /project/app +COPY auth/src ./src + CMD ["bun", "src/index.ts"] diff --git a/app/server/services/ts/user/Dockerfile b/app/server/services/ts/user/Dockerfile index 75f517d..64cf526 100644 --- a/app/server/services/ts/user/Dockerfile +++ b/app/server/services/ts/user/Dockerfile @@ -2,13 +2,17 @@ FROM oven/bun:1.0-slim WORKDIR /project COPY package.json . -COPY tsconfig.json . -COPY common ./common RUN bun install WORKDIR /project/app -COPY user/package.json . -COPY user/src src +COPY auth/package.json . RUN bun install +WORKDIR /project +COPY tsconfig.json . +COPY common ./common + +WORKDIR /project/app +COPY auth/src ./src + CMD ["bun", "src/index.ts"] From 32e6b7f9acdf9f26cad5525943b69d7063991522 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 20 Feb 2026 08:41:03 -0600 Subject: [PATCH 076/204] added support for .env files in ts services --- .gitignore | 3 --- app/server/services/ts/.gitignore | 8 -------- app/server/services/ts/Dockerfile | 18 ------------------ app/server/services/ts/api-gateway/.env.local | 0 app/server/services/ts/api-gateway/Dockerfile | 2 +- app/server/services/ts/auth/.env.local | 0 app/server/services/ts/auth/Dockerfile | 2 +- app/server/services/ts/user/.env.local | 0 app/server/services/ts/user/Dockerfile | 2 +- 9 files changed, 3 insertions(+), 32 deletions(-) delete mode 100644 app/server/services/ts/Dockerfile create mode 100644 app/server/services/ts/api-gateway/.env.local create mode 100644 app/server/services/ts/auth/.env.local create mode 100644 app/server/services/ts/user/.env.local diff --git a/.gitignore b/.gitignore index fa62064..df7d265 100644 --- a/.gitignore +++ b/.gitignore @@ -135,11 +135,8 @@ celerybeat.pid *.sage.py # Environments -.envrc .venv -env/ venv/ -ENV/ env.bak/ venv.bak/ diff --git a/app/server/services/ts/.gitignore b/app/server/services/ts/.gitignore index 952c01d..eb08f6e 100644 --- a/app/server/services/ts/.gitignore +++ b/app/server/services/ts/.gitignore @@ -102,14 +102,6 @@ web_modules/ .yarn-integrity -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - # parcel-bundler cache (https://parceljs.org/) .parcel-cache diff --git a/app/server/services/ts/Dockerfile b/app/server/services/ts/Dockerfile deleted file mode 100644 index 64cf526..0000000 --- a/app/server/services/ts/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM oven/bun:1.0-slim - -WORKDIR /project -COPY package.json . -RUN bun install - -WORKDIR /project/app -COPY auth/package.json . -RUN bun install - -WORKDIR /project -COPY tsconfig.json . -COPY common ./common - -WORKDIR /project/app -COPY auth/src ./src - -CMD ["bun", "src/index.ts"] diff --git a/app/server/services/ts/api-gateway/.env.local b/app/server/services/ts/api-gateway/.env.local new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/ts/api-gateway/Dockerfile b/app/server/services/ts/api-gateway/Dockerfile index 64cf526..159587f 100644 --- a/app/server/services/ts/api-gateway/Dockerfile +++ b/app/server/services/ts/api-gateway/Dockerfile @@ -15,4 +15,4 @@ COPY common ./common WORKDIR /project/app COPY auth/src ./src -CMD ["bun", "src/index.ts"] +CMD ["bun", "--no-env-file", "src/index.ts"] diff --git a/app/server/services/ts/auth/.env.local b/app/server/services/ts/auth/.env.local new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/ts/auth/Dockerfile b/app/server/services/ts/auth/Dockerfile index 64cf526..159587f 100644 --- a/app/server/services/ts/auth/Dockerfile +++ b/app/server/services/ts/auth/Dockerfile @@ -15,4 +15,4 @@ COPY common ./common WORKDIR /project/app COPY auth/src ./src -CMD ["bun", "src/index.ts"] +CMD ["bun", "--no-env-file", "src/index.ts"] diff --git a/app/server/services/ts/user/.env.local b/app/server/services/ts/user/.env.local new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/ts/user/Dockerfile b/app/server/services/ts/user/Dockerfile index 64cf526..159587f 100644 --- a/app/server/services/ts/user/Dockerfile +++ b/app/server/services/ts/user/Dockerfile @@ -15,4 +15,4 @@ COPY common ./common WORKDIR /project/app COPY auth/src ./src -CMD ["bun", "src/index.ts"] +CMD ["bun", "--no-env-file", "src/index.ts"] From 8f1806c3d3b66af51fe2854b71798f567b4eed3a Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 20 Feb 2026 10:21:21 -0600 Subject: [PATCH 077/204] using http proxy in gateway + env file support --- app/client/src/App.tsx | 6 +- app/server/services/ts/api-gateway/Dockerfile | 4 +- .../services/ts/api-gateway/package.json | 3 + .../services/ts/api-gateway/src/index.ts | 78 +++++++------------ app/server/services/ts/auth/src/index.ts | 21 +++-- app/server/services/ts/auth/src/logic.ts | 3 +- .../ts/auth/src/{dto.ts => parsing.ts} | 0 app/server/services/ts/common/hooks.ts | 2 +- app/server/services/ts/user/Dockerfile | 4 +- app/server/services/ts/user/src/index.ts | 2 +- 10 files changed, 51 insertions(+), 72 deletions(-) rename app/server/services/ts/auth/src/{dto.ts => parsing.ts} (100%) diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index bdb6c4f..324684e 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -171,7 +171,7 @@ function LoginPage({ onLogin }: LoginPageProps) { setErrorMessage('') setIsSubmitting(true) - const result = await requestAuth('/api/auth/login', { + const result = await requestAuth('/api/login', { email: email.trim(), password, }) @@ -266,7 +266,7 @@ function SignUpPage({ onSignup }: SignUpPageProps) { email: normalizedEmail, } - const result = await requestAuth('/api/auth/signup', { ...cleanedUser, password }) + const result = await requestAuth('/api/signup', { ...cleanedUser, password }) setIsSubmitting(false) if (result.status >= 400) { @@ -274,7 +274,7 @@ function SignUpPage({ onSignup }: SignUpPageProps) { return } - const tokenReq = await requestAuth('/api/auth/login', { + const tokenReq = await requestAuth('/api/login', { email: normalizedEmail, password, }) diff --git a/app/server/services/ts/api-gateway/Dockerfile b/app/server/services/ts/api-gateway/Dockerfile index 159587f..c4bb337 100644 --- a/app/server/services/ts/api-gateway/Dockerfile +++ b/app/server/services/ts/api-gateway/Dockerfile @@ -5,7 +5,7 @@ COPY package.json . RUN bun install WORKDIR /project/app -COPY auth/package.json . +COPY api-gateway/package.json . RUN bun install WORKDIR /project @@ -13,6 +13,6 @@ COPY tsconfig.json . COPY common ./common WORKDIR /project/app -COPY auth/src ./src +COPY api-gateway/src ./src CMD ["bun", "--no-env-file", "src/index.ts"] diff --git a/app/server/services/ts/api-gateway/package.json b/app/server/services/ts/api-gateway/package.json index 9454a02..696ad0b 100644 --- a/app/server/services/ts/api-gateway/package.json +++ b/app/server/services/ts/api-gateway/package.json @@ -5,5 +5,8 @@ "dev": "bun --watch src/index.ts", "build": "tsc", "start": "bun dist/index.js" + }, + "dependencies": { + "http-proxy-middleware": "^3.0.5" } } diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 0215d0f..6e9972d 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -2,60 +2,31 @@ import { PORT } from "@/port"; import { buildCorsConfig } from "@/corsUtil"; import { onExit } from "@/hooks"; import express from "express"; - -const AUTH_SERVICE_ADDR = process.env.AUTH_SERVICE_ADDR || "http://localhost:8000"; +import { createProxyMiddleware } from 'http-proxy-middleware'; const app = express(); -app.use(buildCorsConfig(undefined)); -app.use(express.json()); - -async function forwardAuthRequest(req: express.Request, res: express.Response, path: string) { - try { - const authResponse = await fetch(`${AUTH_SERVICE_ADDR}${path}`, { - method: req.method, - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(req.body ?? {}), - }); - - const responseText = await authResponse.text(); - const responseType = authResponse.headers.get("content-type") || ""; - res.status(authResponse.status); - - if (responseType.includes("application/json")) { - res.type("application/json"); - try { - res.send(JSON.parse(responseText)); - return; - } catch { - res.send(responseText); - return; - } - } - res.send(responseText); - } catch (error) { - const message = error instanceof Error ? error.message : "Unable to reach auth service."; - res.status(502).json({ ok: false, error: message }); - } -} - -app.post("/api/auth/signup", async (req: express.Request, res: express.Response) => { - await forwardAuthRequest(req, res, "/api/auth/signup"); -}); - -app.post("/api/auth/login", async (req: express.Request, res: express.Response) => { - await forwardAuthRequest(req, res, "/api/auth/login"); -}); - -const server = app.listen(PORT, () => { - console.log(`API Gateway running on port ${PORT}`); -}); -onExit(() => server.close()); +app.use( + createProxyMiddleware({ + pathFilter: "/api/signup", + target: process.env.AUTH_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { "^/api/signup": "/signup" } + }) +); + +app.use( + createProxyMiddleware({ + pathFilter: "/api/login", + target: process.env.AUTH_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { + "^/api/login": "/login", + }, + }) +); app.get("/health", async (_req: express.Request, res: express.Response) => { -//test endpoint const result: { [service: string]: string } = {}; const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x), @@ -71,3 +42,12 @@ app.get("/health", async (_req: express.Request, res: express.Response) => { } res.json(result); }); + + +app.use(buildCorsConfig(undefined)); +app.use(express.json()); + +const server = app.listen(PORT, () => { + console.log(`API Gateway running on port ${PORT}`); +}); +onExit(async () => await server.close()); diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts index 7399afa..36bb800 100644 --- a/app/server/services/ts/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -5,7 +5,7 @@ import { onExit } from "@/hooks"; import { buildCorsConfig } from "@/corsUtil"; import { signup, login } from "./logic"; import type { LoginBody, SignupBody } from "./types"; -import { parseLoginBody, parseSignupBody } from "./dto"; +import { parseLoginBody, parseSignupBody } from "./parsing"; const pool = mysql.createPool({ host: process.env.MYSQL_HOST, @@ -21,7 +21,7 @@ const app = express(); app.use(express.json()); app.use(buildCorsConfig(undefined)); -app.post("/api/auth/signup", async (req, res) => { +app.post("/signup", async (req, res) => { let body: SignupBody; try { body = parseSignupBody(req.body); @@ -33,7 +33,7 @@ app.post("/api/auth/signup", async (req, res) => { signup(body, res, pool); }); -app.post("/api/auth/login", async (req, res) => { +app.post("/login", async (req, res) => { let body: LoginBody; try { body = parseLoginBody(req.body); @@ -45,19 +45,16 @@ app.post("/api/auth/login", async (req, res) => { login(body, res, pool); }); -app.get("/health", (_req, res) => { - res.send("ok"); -}); - -app.get("/api/health", (_req, res) => { - res.json({ ok: true, status: "up" }); +app.get("/health", (_, res) => { + res.send({ok: true}); }); const server = app.listen(PORT, () => { console.log(`Auth Service running on port ${PORT}`); }); -onExit(() => { - server.close(); - pool.end(); +onExit(async () => { + await new Promise((res) => server.close(res)); + await pool.end(); + process.exit(0); }); diff --git a/app/server/services/ts/auth/src/logic.ts b/app/server/services/ts/auth/src/logic.ts index 06bbd21..2c5c2af 100644 --- a/app/server/services/ts/auth/src/logic.ts +++ b/app/server/services/ts/auth/src/logic.ts @@ -70,8 +70,7 @@ export async function login(body: LoginBody, res, pool): void { let user: User; try { user = await getUserWithPasswordByEmail(body.email, pool); - } catch (e) { - console.error(e); + } catch { res.status(401).json({ error: "Invalid email or password." }); return; } diff --git a/app/server/services/ts/auth/src/dto.ts b/app/server/services/ts/auth/src/parsing.ts similarity index 100% rename from app/server/services/ts/auth/src/dto.ts rename to app/server/services/ts/auth/src/parsing.ts diff --git a/app/server/services/ts/common/hooks.ts b/app/server/services/ts/common/hooks.ts index aea4f13..d0f66dd 100644 --- a/app/server/services/ts/common/hooks.ts +++ b/app/server/services/ts/common/hooks.ts @@ -1,3 +1,3 @@ -export function onExit(callback: () => void) { +export function onExit(callback: () => Promise) { process.on("SIGTERM", callback); } diff --git a/app/server/services/ts/user/Dockerfile b/app/server/services/ts/user/Dockerfile index 159587f..45850ac 100644 --- a/app/server/services/ts/user/Dockerfile +++ b/app/server/services/ts/user/Dockerfile @@ -5,7 +5,7 @@ COPY package.json . RUN bun install WORKDIR /project/app -COPY auth/package.json . +COPY user/package.json . RUN bun install WORKDIR /project @@ -13,6 +13,6 @@ COPY tsconfig.json . COPY common ./common WORKDIR /project/app -COPY auth/src ./src +COPY user/src ./src CMD ["bun", "--no-env-file", "src/index.ts"] diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index d930eb9..0d62f43 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -10,7 +10,7 @@ app.use(buildCorsConfig()); const server = app.listen(PORT, () => { console.log(`User Service running on port ${PORT}`); }); -onExit(() => server.close()); +onExit(async () => await server.close()); //test endpoint app.get("/health", (req: express.Request, res: express.Response) => { From 2c693de49e8bbb0dd2c568e9922492f21bf3ee44 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 20 Feb 2026 11:56:16 -0600 Subject: [PATCH 078/204] Visualization backend: enabled API endpoints with dummy data --- app/client/src/api/ManagerAPI.tsx | 115 ++++++------------ app/client/src/pages/DashboardPage.tsx | 1 - .../services/python/analytics/src/main.py | 105 +++++++++++++++- .../services/ts/api-gateway/src/index.ts | 29 +++++ 4 files changed, 173 insertions(+), 77 deletions(-) diff --git a/app/client/src/api/ManagerAPI.tsx b/app/client/src/api/ManagerAPI.tsx index 1976d3c..e1b681f 100644 --- a/app/client/src/api/ManagerAPI.tsx +++ b/app/client/src/api/ManagerAPI.tsx @@ -57,60 +57,25 @@ async function getSavingsContribChartData(period: string): Promise { if (!["w", "m", "y"].includes(period)) { throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); } - // const response = await axios.get(`${BASE_URL}/charts/income-flow?period=${period}`); - // if (response.status !== 200) { - // throw new Error(`Failed to fetch income flow chart data: ${response.statusText}`); - // } - //return response.data; - const dataTest = { - nodes: [ - { name: 'Salary' }, - { name: 'Pokemon Cards' }, - { name: 'Total Income' }, - { name: 'Food' }, - { name: 'Mortgage' }, - ], - links: [ - { source: 0, target: 1, value: 3728.3 }, - { source: 0, target: 2, value: 354170 }, - { source: 2, target: 3, value: 62429 }, - { source: 2, target: 4, value: 291741 }, - ], - }; - return dataTest; + const response = await axios.get(`${BASE_URL}/charts/incomeflow?period=${period}`); + if (response.status !== 200) { + throw new Error(`Failed to fetch income flow chart data: ${response.statusText}`); + } + return response.data; + // const dataTest = { + // nodes: [ + // { name: 'Salary' }, + // { name: 'Pokemon Cards' }, + // { name: 'Total Income' }, + // { name: 'Food' }, + // { name: 'Mortgage' }, + // ], + // links: [ + // { source: 0, target: 1, value: 3728.3 }, + // { source: 0, target: 2, value: 354170 }, + // { source: 2, target: 3, value: 62429 }, + // { source: 2, target: 4, value: 291741 }, + // ], + // }; + // return dataTest; } catch (error) { console.error("Error fetching income flow chart data:", error); throw error; diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 071bc58..326776f 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -4,7 +4,6 @@ import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScal import { Pie, Line, Bar } from 'react-chartjs-2'; import AccountCard from '@/components/AccountCard'; import TransactionTable from '@/components/TransactionTable'; -//import {Button} from '@/components/Button'; import LoadingSpinner from '@/components/LoadingSpinner'; import SankeyChart from '@/components/SankeyChart'; import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py index 727d79c..b8e59f7 100644 --- a/app/server/services/python/analytics/src/main.py +++ b/app/server/services/python/analytics/src/main.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Query from pydantic import BaseModel from fastapi.middleware.cors import CORSMiddleware import pandas as pd @@ -23,6 +23,109 @@ def test_endpoint(): return 'ok' +@app.get('/charts/savings') +async def get_savings(period: str): + out_period = '' + #random dummy data + if period == 'w': + dates = pd.date_range(end=pd.Timestamp.today(), periods=7).strftime('%Y-%m-%d').tolist() + savings = np.random.randint(50, 200, size=7).tolist() + out_period = 'Weekly' + elif period == 'm': + dates = pd.date_range(end=pd.Timestamp.today(), periods=30).strftime('%Y-%m-%d').tolist() + savings = np.random.randint(50, 200, size=30).tolist() + out_period = 'Monthly' + elif period == 'y': + dates = pd.date_range(end=pd.Timestamp.today(), periods=12, freq='M').strftime('%Y-%m').tolist() + savings = np.random.randint(1000, 5000, size=12).tolist() + out_period = 'Yearly' + else: + raise HTTPException(status_code=400, detail="Invalid period. Use 'weekly', 'monthly', or 'yearly'.") + + return {'labels': dates, 'datasets': [{'label': f'{out_period} Savings', 'data': savings}]} + + + +@app.get('/charts/incomeflow') +async def get_income_flow(period: str = Query(default='w', enum=['w', 'm', 'y'])): + NUM_R_D = 5 + NUM_R_C = 10 + + transaction_categories_pos = ['salary', 'e-transfer', 'cash'] + transaction_categories_neg = ['food', 'fuel', 'mortgage', 'entertainment', 'tax'] + transactions = [] + date_standard = pd.Timestamp.today() + for category in transaction_categories_pos: + for i in range(np.random.randint(1, NUM_R_D)): + transactions.append({'category': category, 'value': np.random.randint(1000, 5000), 'date' : f'{date_standard.year}-{np.random.randint(1, 13):02d}-{np.random.randint(1, 29):02d}'}) + for category in transaction_categories_neg: + for i in range(np.random.randint(1, NUM_R_C)): + transactions.append({'category': category, 'value': np.random.randint(-500, -1), 'date' : f'{date_standard.year}-{np.random.randint(1, 13):02d}-{np.random.randint(1, 29):02d}'}) + + df = pd.DataFrame(transactions) + df['date'] = pd.to_datetime(df['date']) + + target_period = '' + if period == 'w': + target_period = 'W' + elif period == 'm': + target_period = 'M' + elif period == 'y': + target_period = 'Y' + df['period'] = df['date'].dt.to_period(target_period) + df_agg = df.groupby(['category']).agg({'value': 'sum'}).reset_index() + + total_income = df_agg[df_agg['value']>0]['value'].sum() + total_expenses = abs(df_agg[df_agg['value']<0]['value'].sum()) + + node_set = set() + node_set.add('total income') + + for _, row in df_agg.iterrows(): + node_set.add(row['category']) + + if total_income > total_expenses: + node_set.add('unspent') + elif total_expenses > total_income: + node_set.add('overspent') + node_set.add('savings') + + nodes_list = list(node_set) + node_to_index = {node: idx for idx, node in enumerate(nodes_list)} + + links = [] + + for _, row in df_agg[df_agg['value'] > 0].iterrows(): + links.append({ + 'source': node_to_index[row['category']], + 'target': node_to_index['total income'], + 'value': int(row['value']) + }) + + for _, row in df_agg[df_agg['value'] < 0].iterrows(): + links.append({ + 'source': node_to_index['total income'], + 'target': node_to_index[row['category']], + 'value': int(abs(row['value'])) + }) + + if total_income > total_expenses: + links.append({ + 'source': node_to_index['total income'], + 'target': node_to_index['unspent'], + 'value': int(total_income - total_expenses) + }) + elif total_expenses > total_income: + links.append({ + 'source': node_to_index['savings'], + 'target': node_to_index['overspent'], + 'value': int(total_expenses - total_income) + }) + + return { + 'nodes': [{'name': node} for node in nodes_list], + 'links': links + } if __name__ == "__main__": port = int(os.getenv("PORT", 8000)) diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index b52c026..b783de7 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -51,3 +51,32 @@ app.get('/charts/expenses', async (req: express.Request, res: express.Response) }); res.json(response); }); + + + +app.get('/charts/savings', async (req: express.Request, res: express.Response) => { + console.log("Received request for savings chart data with query:", req.query); + const period = req.query.period as string; + const response = await fetch(`${process.env.ANALYTICS_SERVICE_ADDR}/charts/savings?period=${period}`) + .then((res) => res.json()) + .catch((err) => { + console.error("Error fetching savings chart data:", err); + res.status(500).json({ error: "Failed to fetch savings chart data" }); + }); + res.json(response); +}); + + + + +app.get('/charts/incomeflow', async (req: express.Request, res: express.Response) => { + console.log("Received request for income chart data with query:", req.query); + const period = req.query.period as string; + const response = await fetch(`${process.env.ANALYTICS_SERVICE_ADDR}/charts/incomeflow?period=${period}`) + .then((res) => res.json()) + .catch((err) => { + console.error("Error fetching income chart data:", err); + res.status(500).json({ error: "Failed to fetch income chart data" }); + }); + res.json(response); +}); From 4dfc1850689b721febb74ab6ba2a206b86c8ad86 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Sat, 21 Feb 2026 19:58:06 -0600 Subject: [PATCH 079/204] Client: Refactored dashboard chart section and table --- .../src/components/DashboardChartSection.tsx | 286 ++++++++++++++++++ .../src/components/TransactionTable.tsx | 6 - app/client/src/pages/DashboardPage.tsx | 276 +---------------- app/server/api-gateway-s/.gitignore | 1 + app/server/auth-s/.gitignore | 1 + app/server/user-s/.gitignore | 1 + 6 files changed, 293 insertions(+), 278 deletions(-) create mode 100644 app/client/src/components/DashboardChartSection.tsx create mode 100644 app/server/api-gateway-s/.gitignore create mode 100644 app/server/auth-s/.gitignore create mode 100644 app/server/user-s/.gitignore diff --git a/app/client/src/components/DashboardChartSection.tsx b/app/client/src/components/DashboardChartSection.tsx new file mode 100644 index 0000000..6b6d8e2 --- /dev/null +++ b/app/client/src/components/DashboardChartSection.tsx @@ -0,0 +1,286 @@ +import React, { useEffect, useState } from 'react' +import type { Transaction } from '@/types/Transaction' +import { Pie, Line, Bar } from 'react-chartjs-2'; +import LoadingSpinner from '@/components/LoadingSpinner'; +import SankeyChart from '@/components/SankeyChart'; +import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; +import type { SankeyData } from 'recharts/types/chart/Sankey'; +import type { ChartData } from 'chart.js'; +function DashboardChartSection() { + //Active chart state - this just determines which chart is displayed in the holder - change this later to potentially load up all charts at once if latency is good + const [activeChart, setActiveChart] = useState<'expenses' | 'savings' | 'income'>('expenses'); + + //Active chart period state - this determines over what timeframe the chart is displayed - weekly, monthly, or yearly - default to monthly + const [selectedPeriod, setSelectedPeriod] = useState<'w' | 'm' | 'y'>('m'); + + //Chart data state - this is used to determine whether to put a spinner in place of a chart while data is being fetched from API + const [expensesData, setExpensesData] = useState | null>(null); + const [savingsData, setsavingsData] = useState | null>(null); + const [incomeData, setIncomeData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const data = { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 20, + circumference: 360, + }] + }; + + + const options = { + responsive: true, + plugins: { + legend: { + position: 'right' as const, + }, + title: { + display: true, + text: 'Expenses Breakdown', + font: { + size: 24, + weight: 'bold' as const + } + }, + layout: { + padding: 500, + + }, + legendDistance: { + padding: 50 + } + + }, + }; + + const fetchExpensesData = async () => { + setIsLoading(true); + try { + const data = await getTestExpensesData(selectedPeriod); + setExpensesData(data); + } catch (error) { + console.error("Failed to fetch expenses data:", error); + } finally { + setIsLoading(false); + } + }; + + const fetchSavingsData = async () => { + setIsLoading(true); + try { + const data = await getTestSavingsContribData(selectedPeriod); + setsavingsData(data); + } catch (error) { + console.error("Failed to fetch savings data:", error); + } finally { + setIsLoading(false); + } + }; + + const fetchIncomeData = async () => { + setIsLoading(true); + try { + const data = await getTestIncomeFlowData(selectedPeriod); + //console.log("Fetched income flow data:", data); + setIncomeData(data); + } catch (error) { + console.error("Failed to fetch income data:", error); + } finally { + setIsLoading(false); + } + }; + + + useEffect(() => { + switch(activeChart) { + case 'expenses': + fetchExpensesData(); + break; + case 'savings': + fetchSavingsData(); + break; + case 'income': + fetchIncomeData(); + break; + default: + fetchExpensesData(); + } + }, [activeChart, selectedPeriod]); + + + //Temporary test data for expenses - delete once API works. This is just to test graph components + const getTestExpensesData = async (period: 'w' | 'm' | 'y'): Promise> => { + //Try to reach API first, get synthetic data if fails + try{ + const response = await getExpensesChartData(period); + return response; + }catch(error) { + console.error("Error fetching expenses chart data:", error); + } + return { + labels: ['Mon'], + datasets: [{ + label: 'Placeholder Expenses', + data: [125], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }] + }; + }; + + + const getTestSavingsContribData = async (period: 'w' | 'm' | 'y'): Promise> => { + try{ + const response = await getSavingsContribChartData(period); + return response; + }catch(error) { + console.error("Error fetching savings contribution chart data:", error); + } + return { + labels: ['Mon'], + datasets: [{ + label: 'Placeholder Savings Contribution', + data: [125], + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }] + }; + } + + const getTestIncomeFlowData = async (period: 'w' | 'm' | 'y'): Promise => { + try{ + const response = await getIncomeFlowChartData(period); + //console.log("Received income flow chart data:", response); + return response; + }catch(error) { + console.error("Error fetching income flow chart data:", error); + } + console.log("Using placeholder income flow chart data"); + return { + nodes: [], + links: [], + }; + } + + +const expensesBarOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Expenses Over Time', + font: { + size: 24, + weight: 'bold' as const + } + } + }, +}; + +const savingsLineOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Expenses Over Time', + font: { + size: 24, + weight: 'bold' as const + } + } + }, +}; + + +//Renders a chart based on activeChart - puts a spinner in place while data is being fetched or if no data is available for any reason + const renderChart = () => { + switch(activeChart) { + case 'expenses': + if (isLoading || !expensesData) { + return ( +
+ < LoadingSpinner /> +
+ ); + } + return ( +
+ +
+ ); + case 'savings': + if (isLoading || !savingsData) { + return ( +
+ < LoadingSpinner /> +
+ ); + } + return ( +
+ +
+ ); + case 'income': + if (isLoading || !incomeData) { + return ( +
+ < LoadingSpinner /> +
+ ); + } + return ( +
+ +
+ ); + default: + return ( +
+ {/* figure out a default case in case of an error - can have a placeholder or a spinner chart (loading spinner*/} + < LoadingSpinner /> +
+ ); + } + }; + return ( + <> +
+ + + +
+
+
+ {/* This chart is here just to test all the graph components */} + + + + {renderChart()} +
+ +
+
+
+ + ) +} + +export default DashboardChartSection \ No newline at end of file diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index abf8b46..278c5f7 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -94,11 +94,6 @@ const transactions: Transaction[] = [ } ]; function TransactionTable() { - return ( - - ) -} -function Grid() { return (
{/* Header */} @@ -153,7 +148,6 @@ function Grid() { ))}
- ) } export default TransactionTable \ No newline at end of file diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 326776f..37185dc 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -8,266 +8,14 @@ import LoadingSpinner from '@/components/LoadingSpinner'; import SankeyChart from '@/components/SankeyChart'; import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; import type { SankeyData } from 'recharts/types/chart/Sankey'; +import ChartSection from '@/components/DashboardChartSection'; +import DashboardChartSection from '@/components/DashboardChartSection'; Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); function DashboardPage() { - //Active chart state - this just determines which chart is displayed in the holder - change this later to potentially load up all charts at once if latency is good - const [activeChart, setActiveChart] = useState<'expenses' | 'savings' | 'income'>('expenses'); - - //Active chart period state - this determines over what timeframe the chart is displayed - weekly, monthly, or yearly - default to monthly - const [selectedPeriod, setSelectedPeriod] = useState<'w' | 'm' | 'y'>('m'); - - //Chart data state - this is used to determine whether to put a spinner in place of a chart while data is being fetched from API - const [expensesData, setExpensesData] = useState | null>(null); - const [savingsData, setsavingsData] = useState | null>(null); - const [incomeData, setIncomeData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const data = { - labels: [ - 'Red', - 'Blue', - 'Yellow' - ], - datasets: [{ - label: 'My First Dataset', - data: [300, 50, 100], - backgroundColor: [ - 'rgb(255, 99, 132)', - 'rgb(54, 162, 235)', - 'rgb(255, 205, 86)' - ], - hoverOffset: 20, - circumference: 360, - }] - }; - - - const options = { - responsive: true, - plugins: { - legend: { - position: 'right' as const, - }, - title: { - display: true, - text: 'Expenses Breakdown', - font: { - size: 24, - weight: 'bold' as const - } - }, - layout: { - padding: 500, - - }, - legendDistance: { - padding: 50 - } - - }, - }; - - - const fetchExpensesData = async () => { - setIsLoading(true); - try { - const data = await getTestExpensesData(selectedPeriod); - setExpensesData(data); - } catch (error) { - console.error("Failed to fetch expenses data:", error); - } finally { - setIsLoading(false); - } - }; - - const fetchSavingsData = async () => { - setIsLoading(true); - try { - const data = await getTestSavingsContribData(selectedPeriod); - setsavingsData(data); - } catch (error) { - console.error("Failed to fetch savings data:", error); - } finally { - setIsLoading(false); - } - }; - - const fetchIncomeData = async () => { - setIsLoading(true); - try { - const data = await getTestIncomeFlowData(selectedPeriod); - //console.log("Fetched income flow data:", data); - setIncomeData(data); - } catch (error) { - console.error("Failed to fetch income data:", error); - } finally { - setIsLoading(false); - } - }; - - - useEffect(() => { - switch(activeChart) { - case 'expenses': - fetchExpensesData(); - break; - case 'savings': - fetchSavingsData(); - break; - case 'income': - fetchIncomeData(); - break; - default: - fetchExpensesData(); - } - }, [activeChart, selectedPeriod]); - - - //Temporary test data for expenses - delete once API works. This is just to test graph components - const getTestExpensesData = async (period: 'w' | 'm' | 'y'): Promise> => { - //Try to reach API first, get synthetic data if fails - try{ - const response = await getExpensesChartData(period); - return response; - }catch(error) { - console.error("Error fetching expenses chart data:", error); - } - return { - labels: ['Mon'], - datasets: [{ - label: 'Placeholder Expenses', - data: [125], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }] - }; - }; - - - const getTestSavingsContribData = async (period: 'w' | 'm' | 'y'): Promise> => { - try{ - const response = await getSavingsContribChartData(period); - return response; - }catch(error) { - console.error("Error fetching savings contribution chart data:", error); - } - return { - labels: ['Mon'], - datasets: [{ - label: 'Placeholder Savings Contribution', - data: [125], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }] - }; - } - - const getTestIncomeFlowData = async (period: 'w' | 'm' | 'y'): Promise => { - try{ - const response = await getIncomeFlowChartData(period); - //console.log("Received income flow chart data:", response); - return response; - }catch(error) { - console.error("Error fetching income flow chart data:", error); - } - console.log("Using placeholder income flow chart data"); - return { - nodes: [], - links: [], - }; - } - - -const expensesBarOptions = { - responsive: true, - plugins: { - legend: { - position: 'top' as const, - }, - title: { - display: true, - text: 'Expenses Over Time', - font: { - size: 24, - weight: 'bold' as const - } - } - }, -}; - -const savingsLineOptions = { - responsive: true, - plugins: { - legend: { - position: 'top' as const, - }, - title: { - display: true, - text: 'Expenses Over Time', - font: { - size: 24, - weight: 'bold' as const - } - } - }, -}; - - -//Renders a chart based on activeChart - puts a spinner in place while data is being fetched or if no data is available for any reason - const renderChart = () => { - switch(activeChart) { - case 'expenses': - if (isLoading || !expensesData) { - return ( -
- < LoadingSpinner /> -
- ); - } - return ( -
- -
- ); - case 'savings': - if (isLoading || !savingsData) { - return ( -
- < LoadingSpinner /> -
- ); - } - return ( -
- -
- ); - case 'income': - if (isLoading || !incomeData) { - return ( -
- < LoadingSpinner /> -
- ); - } - return ( -
- -
- ); - default: - return ( -
- {/* figure out a default case in case of an error - can have a placeholder or a spinner chart (loading spinner*/} - < LoadingSpinner /> -
- ); - } - }; - + return ( @@ -281,23 +29,7 @@ const savingsLineOptions = {
-
- - - -
-
-
- {/* This chart is here just to test all the graph components */} - - - - {renderChart()} -
-
- -
-
+

Recent Transactions

diff --git a/app/server/api-gateway-s/.gitignore b/app/server/api-gateway-s/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/app/server/api-gateway-s/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/app/server/auth-s/.gitignore b/app/server/auth-s/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/app/server/auth-s/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/app/server/user-s/.gitignore b/app/server/user-s/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/app/server/user-s/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file From c67eb819aae96bf260afb35eebb3551e3bc57fa6 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Sun, 22 Feb 2026 04:37:11 -0600 Subject: [PATCH 080/204] Created functions to validate the form data and functions to get/post data to/from the server. --- app/client/src/App.tsx | 2 +- app/client/src/api/Account.ts | 46 ++++++++++++++ app/client/src/api/Transaction.ts | 9 +++ app/client/src/components/ui/AccountForm.tsx | 56 ++++++++++++----- .../src/components/ui/TransationForm.tsx | 62 +++++++++++++++---- app/client/src/components/ui/userForm.css | 2 +- app/client/src/util/ValidateForms.ts | 42 +++++++++++++ 7 files changed, 190 insertions(+), 29 deletions(-) create mode 100644 app/client/src/api/Account.ts create mode 100644 app/client/src/api/Transaction.ts create mode 100644 app/client/src/util/ValidateForms.ts diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 7d2cc10..028fab9 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -70,7 +70,7 @@ function NotFoundPage() {

- {seen ? ():null} + {seen ? ():null} ) diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts new file mode 100644 index 0000000..d335c7f --- /dev/null +++ b/app/client/src/api/Account.ts @@ -0,0 +1,46 @@ +//Coppied from module +export interface Account{ + id: number; + name: string; + type: string; + balance: number; + value: number; + subtype?: string; + last_updated: Date; +} + +//Sends a request to get different accounts the user has +export async function getUserAccounts(): Promise { + + //Sends a http request and waits for a response + const response = await fetch('127.0.0.1:3000', { + method: 'GET', + headers: { + 'Cookie': document.cookie + } + }) + + //Determine if we were able to retrieve user's data + if(response.ok) { + return response.json() + + } else{ + //Failed to retrieve user data, return empty array + console.error("Error: Failed to retrieve users accounts ", response.status) + return [] + } + +} + +export async function postUserAccount(newAccount:Account) { + + + const response = fetch('/api/accounts', { + method: "POST", + headers: { + 'content-type': '/application/json', + 'Cookie': document.cookie + }, + body: JSON.stringify(newAccount) + } ) +} \ No newline at end of file diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts new file mode 100644 index 0000000..79d1696 --- /dev/null +++ b/app/client/src/api/Transaction.ts @@ -0,0 +1,9 @@ + +//Will implement +export function getTransactions(account_id: number){ + +} + +export function pushTranscations() { + +} \ No newline at end of file diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index 48288d7..89a8146 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; import CsvUpload from "../csvread/CsvUpload"; -import './userForm.css' - +import './UserForm.css' +import {postUserAccount, type Account} from '../../api/account.ts' +import { validateAccountForm } from "../../util/ValidateForms.ts"; interface popupProp{ toggle: () => void; edit: boolean } -const accountCategory = { +export const accountCategory = { SAVING:"Saving", CHEQUING: "Chequing", INVESTMENT: "Investment", @@ -19,8 +20,32 @@ type typeofAccount = keyof typeof accountCategory export default function popupForm({toggle, edit}:popupProp,){ //Submits the account data + const handleSubmit = () => { - toggle() + let subtype = undefined + let interest = undefined + const accountBalence = Number(balence) + + if(accountType === accountCategory.SAVING) { + subtype = formInput.subType + } + + if(accountType === accountCategory.SAVING || accountType === accountCategory.DEBT){ + interest = formInput.interest + } + + //Check account before validating b/c account can be undefined + //Determine if form input for an account is valid + if(accountType && validateAccountForm(formInput.name, accountCategory[accountType], accountBalence,file,subtype,interest)) { + //Create a new account type and post it + const newAccount: Account = {id: 0, name:formInput.name, type: accountType, balance: accountBalence, subtype:"", value:0, last_updated: new Date()} + console.log(newAccount) + postUserAccount(newAccount) + alert("Account " + formInput.name + " has been created") + toggle() + } else { + alert("Please fill out the form") + } } @@ -53,12 +78,15 @@ export default function popupForm({toggle, edit}:popupProp,){ setFile(undefined) } } + + const handleCurrencyBlur = (event:React.ChangeEvent) => { + if(event.target.value !== "") { + setBalence(parseFloat(balence).toFixed(2)) + } + } - //Add when getUserAccounts is implemented - //const userAccounts = getUserAccounts() - - const [formInput, setFormInput] = useState({name: '', type:'', subType:''}) + const [formInput, setFormInput] = useState({name: '', subType:'', interest: 0}) const [file, setFile] = useState(undefined) const[balence, setBalence] = useState("") const[accountType, setAccountType] = useState(undefined) @@ -69,7 +97,7 @@ export default function popupForm({toggle, edit}:popupProp,){ <>
-
handleSubmit() }> +
{edit ? (

Edit Account

):(

Create Account

)} @@ -85,7 +113,7 @@ export default function popupForm({toggle, edit}:popupProp,){

- +

@@ -99,13 +127,13 @@ export default function popupForm({toggle, edit}:popupProp,){

- ): null + ): null } {(accountType === "SAVING") ? ( <> - @@ -118,9 +146,9 @@ export default function popupForm({toggle, edit}:popupProp,){
- + {edit? (): ()}
- +
diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index d8e4148..1d6740b 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -1,6 +1,8 @@ import React, { useState, type ReactHTMLElement } from "react"; import CsvUpload from "../csvread/CsvUpload"; -import './userForm.css' +import './UserForm.css' +import { getUserAccounts, type Account } from "../../api/account"; +import { validateTransactionForm } from "../../util/ValidateForms"; interface popupProp{ toggle: () => void; @@ -8,7 +10,7 @@ interface popupProp{ } -const transferCategory = { +export const transactionCategory = { FOOD: 'Food', HOUSING: "Housing", UTIL: "Utilzities", @@ -17,14 +19,28 @@ const transferCategory = { }; //Gets the keys of the enum -type typeOfTransfers = keyof typeof transferCategory +type typeOfTransaction = keyof typeof transactionCategory //Returns a form of for the user to enter their info export default function popupForm({toggle, edit}:popupProp){ //Handles the submiting the form const handleSubmit = () =>{ - toggle() + + const transferAmount = Number(amount) + + if(selectedType && validateTransactionForm(Number(selectedAccount), selectedType, transferAmount, file)){ + + if(edit){ + alert("Transaction has been edited") + } else { + alert("Transaction has been created") + } + + toggle() + } else { + alert("Please fill out the transaction form") + } } @@ -63,31 +79,51 @@ export default function popupForm({toggle, edit}:popupProp){ } + //Gets user accounts + let userAccounts: Account[] = [] + + //Add when getUserAccounts is implemented + getUserAccounts().then(accounts => { + console.log(accounts) + + //Detemrine accounts exist + if(accounts) { + setAccount(accounts) + } + }) + + //State of the user's account + const [account, setAccount] = useState() + //Holds state of user input - const [selectedType, setSelectedType] = useState(undefined) + const [selectedAccount, setSelectedAccount] = useState("") + const [selectedType, setSelectedType] = useState("") const [amount, setAmount] = useState("") const [file, setFile] = useState(undefined) //Holds the types of transfers - const transCat: typeOfTransfers [] = Object.keys(transferCategory) as typeOfTransfers[]; + const transCat: typeOfTransaction [] = Object.keys(transactionCategory) as typeOfTransaction[]; + + return( <>
-
+
{edit ? (

Edit Transaction

):(

Create Transaction

)} - setSelectedAccount(event.target.value)}> + + {userAccounts.map(account => )}

- setSelectedType(event.target.value as typeOfTransaction)}> - {transCat.map(category => ())} + {transCat.map(category => ())}

@@ -101,9 +137,9 @@ export default function popupForm({toggle, edit}:popupProp){
- {edit ?():()} + {edit ?():()}
- +
) diff --git a/app/client/src/components/ui/userForm.css b/app/client/src/components/ui/userForm.css index 9f35bd4..7856a36 100644 --- a/app/client/src/components/ui/userForm.css +++ b/app/client/src/components/ui/userForm.css @@ -1,5 +1,5 @@ -form { +.popupForm { background-color: lightgrey; padding: 15px; place-content: start; diff --git a/app/client/src/util/ValidateForms.ts b/app/client/src/util/ValidateForms.ts new file mode 100644 index 0000000..2526044 --- /dev/null +++ b/app/client/src/util/ValidateForms.ts @@ -0,0 +1,42 @@ +import { accountCategory } from "../components/ui/AccountForm"; +import { transactionCategory } from "../components/ui/TransationForm"; + +export function validateAccountForm(name:string, accountType:string, balance: number, csvFile?: File, subtype?: string, interest?: number) { + let result = false + + if(name){ + + //Checks if there a cvs file uploaded first + //Add cvs checker here= + if(csvFile){ + result = true + + //Checks if all the manditory fields are valid and optional ones too if they exist + } else if(Object.values(accountCategory).includes(accountType) && balance >= 0 && (subtype == undefined || subtype) && (interest == undefined || (interest >= 0 && interest < 100))) { + result = true + } + console.log(accountType) + console.log(accountCategory) + } + console.log(result) + return result +} + +export function validateTransactionForm(account_id:number, transactionType:string, amount:number, csvFile?:File) { + + let result = false + + //Determine if account was selected + if(account_id) { + + //Add csv checker here to validate + if(csvFile) { + + result = true + } else if(Object.values(transactionCategory).includes(transactionType) && amount > 0) { + result = true + } + } + + return result +} \ No newline at end of file From 8de279b46c6bf348223aa73fe7346bb1594afe3b Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Sun, 22 Feb 2026 14:58:25 -0600 Subject: [PATCH 081/204] Split login and sign up page into separate files, and reorganized client side files --- app/client/src/App.tsx | 441 +++++------------- app/client/src/pages/LoginPage.tsx | 80 ++++ app/client/src/pages/SignUpPage.tsx | 206 ++++++++ app/client/src/pages/authTypes.ts | 28 ++ .../services/database/grantPrivileges.sh | 8 +- app/server/services/ts/auth/src/logic.ts | 5 +- app/server/services/ts/auth/src/queries.ts | 2 +- 7 files changed, 443 insertions(+), 327 deletions(-) create mode 100644 app/client/src/pages/LoginPage.tsx create mode 100644 app/client/src/pages/SignUpPage.tsx create mode 100644 app/client/src/pages/authTypes.ts diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 324684e..aa47d42 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -1,111 +1,151 @@ -import { useState, type FormEvent } from 'react' -import { BrowserRouter as Router, Link, Navigate, Route, Routes } from 'react-router-dom' -import './App.css' - -type AuthUser = { - id: number - email: string - name: string - age: number -} - -type AuthSession = { - token: string - user: AuthUser -} - -type AuthApiResponse = { - ok: boolean - token?: string - user?: AuthUser - error?: string - message?: string -} - -const SESSION_STORAGE_KEY = 'finus-session' -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000' -const MIN_AGE = 1 -const MAX_AGE = 120 +import { useState } from "react"; +import { + BrowserRouter as Router, + Navigate, + Route, + Routes, +} from "react-router-dom"; +import "./App.css"; +import LoginPage from "./pages/LoginPage"; +import SignUpPage from "./pages/SignUpPage"; +import type { AuthApiResponse, AuthSession, AuthUser } from "./pages/authTypes"; + +const SESSION_STORAGE_KEY = "finus-session"; +const API_BASE_URL = + import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000"; function isValidAuthUser(value: unknown): value is AuthUser { - if (!value || typeof value !== 'object') { - return false + if (!value || typeof value !== "object") { + return false; } - const maybeUser = value as AuthUser - return ( - typeof maybeUser.id === 'number' && - typeof maybeUser.email === 'string' && - typeof maybeUser.name === 'string' && - typeof maybeUser.age === 'number' - ) + return typeof (value as { email?: unknown }).email === "string"; } function loadSession(): AuthSession | null { - const raw = localStorage.getItem(SESSION_STORAGE_KEY) + const raw = localStorage.getItem(SESSION_STORAGE_KEY); if (!raw) { - return null + return null; } try { - const parsed = JSON.parse(raw) as AuthSession + const parsed = JSON.parse(raw) as AuthSession; if (!parsed.token || !isValidAuthUser(parsed.user)) { - return null + return null; } - return parsed + return parsed; } catch { - return null + return null; } } function saveSession(session: AuthSession) { - localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session)) + localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session)); } function clearSession() { - localStorage.removeItem(SESSION_STORAGE_KEY) + localStorage.removeItem(SESSION_STORAGE_KEY); +} + +function decodeTokenClaims(token: string): Record | null { + const parts = token.split("."); + if (parts.length < 2) { + return null; + } + + try { + const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + const json = atob(base64); + const parsed = JSON.parse(json) as Record; + return parsed; + } catch { + return null; + } +} + +function resolveUserFromToken( + token: string, + fallbackUser: Partial, +): AuthUser { + const claims = decodeTokenClaims(token); + + const emailFromToken = + claims && typeof claims.email === "string" ? claims.email : undefined; + const nameFromToken = + claims && typeof claims.name === "string" ? claims.name : undefined; + const firstNameFromToken = + claims && typeof claims.first_name === "string" + ? claims.first_name + : undefined; + const lastNameFromToken = + claims && typeof claims.last_name === "string" + ? claims.last_name + : undefined; + const subFromToken = + claims && typeof claims.sub === "string" ? Number(claims.sub) : undefined; + + return { + id: Number.isFinite(subFromToken) ? subFromToken : fallbackUser.id, + email: emailFromToken ?? fallbackUser.email ?? "", + name: nameFromToken ?? fallbackUser.name, + first_name: firstNameFromToken ?? fallbackUser.first_name, + last_name: lastNameFromToken ?? fallbackUser.last_name, + age: fallbackUser.age, + }; } -async function requestAuth(path: string, payload: Record): Promise { +async function requestAuth( + path: string, + payload: Record, +): Promise { try { const response = await fetch(`${API_BASE_URL}${path}`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(payload), - }) + }); - const data = (await response.json().catch(() => null)) as AuthApiResponse | null + const data = (await response + .json() + .catch(() => null)) as AuthApiResponse | null; if (!data) { - return { ok: false, error: 'Invalid response from auth server.' } + return { ok: false, error: "Invalid response from auth server." }; } if (response.status >= 400) { return { ok: false, - error: data.error ?? 'Authentication failed.', - } + error: data.error ?? "Authentication failed.", + }; } return { ...data, ok: true }; } catch { - return { ok: false, error: 'Unable to connect to auth server.' } + return { ok: false, error: "Unable to connect to auth server." }; } } function App() { - const [session, setSession] = useState(() => loadSession()) - - function handleAuthSuccess(nextSession: AuthSession) { - saveSession(nextSession) - setSession(nextSession) + const [session, setSession] = useState(() => + loadSession(), + ); + + function handleAuthSuccess(token: string, fallbackUser: Partial) { + const nextSession: AuthSession = { + token, + user: resolveUserFromToken(token, fallbackUser), + }; + + saveSession(nextSession); + setSession(nextSession); } function handleLogout() { - clearSession() - setSession(null) + clearSession(); + setSession(null); } return ( @@ -117,7 +157,9 @@ function App() { } + element={ + + } /> ) : ( - + ) } /> @@ -135,7 +180,10 @@ function App() { session ? ( ) : ( - + ) } /> @@ -153,262 +201,13 @@ function App() {
- ) -} - -type LoginPageProps = { - onLogin: (session: AuthSession) => void -} - -function LoginPage({ onLogin }: LoginPageProps) { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [errorMessage, setErrorMessage] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - - async function handleSubmit(event: FormEvent) { - event.preventDefault() - setErrorMessage('') - setIsSubmitting(true) - - const result = await requestAuth('/api/login', { - email: email.trim(), - password, - }) - - setIsSubmitting(false) - - if (!result.token) { - setErrorMessage(result.error ?? 'Login failed.') - return - } - - onLogin({ - token: result.token, - user: result.user, - }) - } - - return ( -
-
-

Finus

-

Welcome back

-

Log in to continue managing your finances.

- -
- - setEmail(event.target.value)} - placeholder="you@example.com" - required - /> - - - setPassword(event.target.value)} - placeholder="Your password" - required - /> - - {errorMessage ?

{errorMessage}

: null} - - -
- -

- New here? Create an account -

-
-
- ) -} - -type SignUpPageProps = { - onSignup: (session: AuthSession) => void -} - -function SignUpPage({ onSignup }: SignUpPageProps) { - const [username, setUsername] = useState('') - const [firstName, setFirstName] = useState('') - const [lastName, setLastName] = useState('') - const [age, setAge] = useState('') - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [errorMessage, setErrorMessage] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - - async function handleSubmit(event: FormEvent) { - event.preventDefault() - setErrorMessage('') - - const cleanedUsername = username.trim() - const cleanedFirstName = firstName.trim() - const cleanedLastName = lastName.trim() - const parsedAge = Number(age) - const normalizedEmail = email.trim().toLowerCase() - - setIsSubmitting(true) - const cleanedUser = { - username: cleanedUsername, - first_name: cleanedFirstName, - last_name: cleanedLastName, - age: parsedAge, - email: normalizedEmail, - } - - const result = await requestAuth('/api/signup', { ...cleanedUser, password }) - setIsSubmitting(false) - - if (result.status >= 400) { - setErrorMessage(result.error ?? 'Signup failed.') - return - } - - const tokenReq = await requestAuth('/api/login', { - email: normalizedEmail, - password, - }) - - - onSignup({ - token: tokenReq.token, - user: cleanedUser, - }) - } - - return ( -
-
-

Finus

-

Create account

-

Set up your account in less than a minute.

- -
-
- Profile -
-
- - setUsername(event.target.value)} - placeholder="johnd" - required - /> -
- -
- - setFirstName(event.target.value)} - placeholder="John" - required - /> -
- -
- - setLastName(event.target.value)} - placeholder="Doe" - required - /> -
- -
- - setAge(event.target.value)} - placeholder="21" - required - /> -
-
-
- -
- Account -
-
- - setEmail(event.target.value)} - placeholder="you@example.com" - required - /> -
- -
- - setPassword(event.target.value)} - placeholder="At least 8 characters" - required - /> -
- -
- - setConfirmPassword(event.target.value)} - placeholder="Repeat password" - required - /> -
-
-
- - {errorMessage ?

{errorMessage}

: null} - - -
- -

- Already have an account? Log in -

-
-
- ) + ); } type DashboardPageProps = { - session: AuthSession - onLogout: () => void -} + session: AuthSession; + onLogout: () => void; +}; function DashboardPage({ session, onLogout }: DashboardPageProps) { return ( @@ -417,15 +216,15 @@ function DashboardPage({ session, onLogout }: DashboardPageProps) {

Finus

Signed in

- Hello {session.user.first_name}. You are logged in as{' '} - {session.user.email}. + Hello {session.user.first_name ?? session.user.name ?? "there"}. You + are logged in as {session.user.email}.

- ) + ); } -export default App +export default App; diff --git a/app/client/src/pages/LoginPage.tsx b/app/client/src/pages/LoginPage.tsx new file mode 100644 index 0000000..9807c27 --- /dev/null +++ b/app/client/src/pages/LoginPage.tsx @@ -0,0 +1,80 @@ +import { useState, type FormEvent } from "react"; +import { Link } from "react-router-dom"; +import type { AuthUser, RequestAuth } from "./authTypes"; + +type LoginPageProps = { + onLogin: (token: string, fallbackUser: Partial) => void; + requestAuth: RequestAuth; +}; + +function LoginPage({ onLogin, requestAuth }: LoginPageProps) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + setErrorMessage(""); + setIsSubmitting(true); + + const normalizedEmail = email.trim().toLowerCase(); + const result = await requestAuth("/api/login", { + email: normalizedEmail, + password, + }); + + setIsSubmitting(false); + + if (!result.ok || !result.token) { + setErrorMessage(result.error ?? "Login failed."); + return; + } + + onLogin(result.token, { email: normalizedEmail }); + } + + return ( +
+
+

Finus

+

Welcome back

+

Log in to continue managing your finances.

+ +
+ + setEmail(event.target.value)} + placeholder="you@example.com" + required + /> + + + setPassword(event.target.value)} + placeholder="Your password" + required + /> + + {errorMessage ?

{errorMessage}

: null} + + +
+ +

+ New here? Create an account +

+
+
+ ); +} + +export default LoginPage; diff --git a/app/client/src/pages/SignUpPage.tsx b/app/client/src/pages/SignUpPage.tsx new file mode 100644 index 0000000..b4e86be --- /dev/null +++ b/app/client/src/pages/SignUpPage.tsx @@ -0,0 +1,206 @@ +import { useState, type FormEvent } from "react"; +import { Link } from "react-router-dom"; +import { MAX_AGE, MIN_AGE, type AuthUser, type RequestAuth } from "./authTypes"; + +type SignUpPageProps = { + onSignup: (token: string, fallbackUser: Partial) => void; + requestAuth: RequestAuth; +}; + +function SignUpPage({ onSignup, requestAuth }: SignUpPageProps) { + const [username, setUsername] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [age, setAge] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + setErrorMessage(""); + + const cleanedUsername = username.trim(); + const cleanedFirstName = firstName.trim(); + const cleanedLastName = lastName.trim(); + const parsedAge = Number(age); + const normalizedEmail = email.trim().toLowerCase(); + + if (password !== confirmPassword) { + setErrorMessage("Passwords do not match."); + return; + } + + if ( + !Number.isInteger(parsedAge) || + parsedAge < MIN_AGE || + parsedAge > MAX_AGE + ) { + setErrorMessage(`Age must be between ${MIN_AGE} and ${MAX_AGE}.`); + return; + } + + setIsSubmitting(true); + + const signupResult = await requestAuth("/api/signup", { + username: cleanedUsername, + first_name: cleanedFirstName, + last_name: cleanedLastName, + age: parsedAge, + email: normalizedEmail, + password, + }); + + if (!signupResult.ok) { + setIsSubmitting(false); + setErrorMessage(signupResult.error ?? "Signup failed."); + return; + } + + const loginResult = await requestAuth("/api/login", { + email: normalizedEmail, + password, + }); + + setIsSubmitting(false); + + if (!loginResult.ok || !loginResult.token) { + setErrorMessage( + loginResult.error ?? "Account created, but auto-login failed.", + ); + return; + } + + onSignup(loginResult.token, { + email: normalizedEmail, + first_name: cleanedFirstName, + last_name: cleanedLastName, + name: `${cleanedFirstName} ${cleanedLastName}`.trim(), + age: parsedAge, + }); + } + + return ( +
+
+

Finus

+

Create account

+

Set up your account in less than a minute.

+ +
+
+ Profile +
+
+ + setUsername(event.target.value)} + placeholder="johnd" + required + /> +
+ +
+ + setFirstName(event.target.value)} + placeholder="John" + required + /> +
+ +
+ + setLastName(event.target.value)} + placeholder="Doe" + required + /> +
+ +
+ + setAge(event.target.value)} + placeholder="21" + required + /> +
+
+
+ +
+ Account +
+
+ + setEmail(event.target.value)} + placeholder="you@example.com" + required + /> +
+ +
+ + setPassword(event.target.value)} + placeholder="At least 8 characters" + required + /> +
+ +
+ + setConfirmPassword(event.target.value)} + placeholder="Repeat password" + required + /> +
+
+
+ + {errorMessage ?

{errorMessage}

: null} + + +
+ +

+ Already have an account? Log in +

+
+
+ ); +} + +export default SignUpPage; diff --git a/app/client/src/pages/authTypes.ts b/app/client/src/pages/authTypes.ts new file mode 100644 index 0000000..76d1c72 --- /dev/null +++ b/app/client/src/pages/authTypes.ts @@ -0,0 +1,28 @@ +export type AuthUser = { + id?: number; + email: string; + name?: string; + first_name?: string; + last_name?: string; + age?: number; +}; + +export type AuthSession = { + token: string; + user: AuthUser; +}; + +export type AuthApiResponse = { + ok: boolean; + token?: string; + error?: string; + message?: string; +}; + +export type RequestAuth = ( + path: string, + payload: Record, +) => Promise; + +export const MIN_AGE = 1; +export const MAX_AGE = 120; diff --git a/app/server/services/database/grantPrivileges.sh b/app/server/services/database/grantPrivileges.sh index a653477..56b7e1c 100755 --- a/app/server/services/database/grantPrivileges.sh +++ b/app/server/services/database/grantPrivileges.sh @@ -1,4 +1,6 @@ -#!/bin/bash -mysql -u root -p$MYSQL_ROOT_PASSWORD -e " -GRANT ALL PRIVILEGES ON finus.* TO '$MYSQL_USER'@'%'; +#!/bin/sh +set -e + +mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e " +L PRIVILEGES ON finus.* TO '$MYSQL_USER'@'%'; " diff --git a/app/server/services/ts/auth/src/logic.ts b/app/server/services/ts/auth/src/logic.ts index 2c5c2af..abc6458 100644 --- a/app/server/services/ts/auth/src/logic.ts +++ b/app/server/services/ts/auth/src/logic.ts @@ -36,17 +36,18 @@ export async function signup(body: SignupBody, res, pool) { return; } } catch { - res.status(500); + res.status(500).json({ error: "Could not validate account state." }); + return; } const passwordHash = await Bun.password.hash(body.password, { algorithm: "bcrypt", }); - console.error(passwordHash); try { await insertUser({ ...body, pw_hash: passwordHash }, pool); } catch (error) { res.status(500).json({ error: error.message }); + return; } res.status(201).json({}); diff --git a/app/server/services/ts/auth/src/queries.ts b/app/server/services/ts/auth/src/queries.ts index 0f31773..9e5c843 100644 --- a/app/server/services/ts/auth/src/queries.ts +++ b/app/server/services/ts/auth/src/queries.ts @@ -54,7 +54,7 @@ export async function insertUser(user: UserWithPassword, pool): void { await connection.execute( ` - INSERT INTO credentials (finus_account_id, pw_hash, salt) + INSERT INTO credentials (finus_account_id, pw_hash) VALUES (?, ?) `, [accountInsert.insertId, user.pw_hash], From c3618dc41efa345c7c86a11ddaecc5cd2643bf13 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Mon, 23 Feb 2026 05:31:24 -0600 Subject: [PATCH 082/204] Finished the api functions for accounts and transactions. --- app/client/src/api/Account.ts | 31 ++++++---- app/client/src/api/Transaction.ts | 56 ++++++++++++++++++- app/client/src/components/ui/AccountForm.tsx | 16 ++++-- .../src/components/ui/TransationForm.tsx | 49 +++++++++++----- app/client/src/type/responseTypes.ts | 5 ++ 5 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 app/client/src/type/responseTypes.ts diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index d335c7f..07ec009 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,3 +1,5 @@ +import type { updateResponse } from "../type/responseTypes"; + //Coppied from module export interface Account{ id: number; @@ -13,7 +15,7 @@ export interface Account{ export async function getUserAccounts(): Promise { //Sends a http request and waits for a response - const response = await fetch('127.0.0.1:3000', { + const response = await fetch('/api/accounts', { method: 'GET', headers: { 'Cookie': document.cookie @@ -21,26 +23,35 @@ export async function getUserAccounts(): Promise { }) //Determine if we were able to retrieve user's data - if(response.ok) { - return response.json() + if(!response.ok) { - } else{ //Failed to retrieve user data, return empty array console.error("Error: Failed to retrieve users accounts ", response.status) - return [] } - + + return response.json() } -export async function postUserAccount(newAccount:Account) { +//Sends a post request to create a user account +export async function postUserAccount(newAccount:Account):Promise { - - const response = fetch('/api/accounts', { + //Create post request and wait for response + const response = await fetch('/api/accounts', { method: "POST", headers: { 'content-type': '/application/json', 'Cookie': document.cookie }, body: JSON.stringify(newAccount) - } ) + } ) + + //Determine if our post was a success + if(response.ok){ + alert("Account " + newAccount.name + " has been created" ) + } else { + alert("Failed to create account " + newAccount.name) + console.error(response.status) + } + + return response.json() } \ No newline at end of file diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 79d1696..b541aa8 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,9 +1,59 @@ +import type { promises } from "dns"; +import { get } from "http"; +import { json } from "stream/consumers"; +import type { updateResponse } from "../type/responseTypes"; -//Will implement -export function getTransactions(account_id: number){ +export interface Transaction{ + id: number; + financialAccount_id: number; + amount: number; + type: string + description?: string; + sender?: string; + recipient?: string; + date: Date; +} +//Sends a GET request to get the list of user transactions for the account +export async function getTransactions(account_id: number): Promise{ + + //Put as object to convert to json when sent in the body + const content = {"id": account_id} + + const response = await fetch("api/transactions", { + method: "GET", + headers: { + "Cookie": document.cookie + }, + body: JSON.stringify(content) + }) + + if(!response.ok){ + alert("Failed to retrieve account's transaction\n") + console.error(response.status) + } + + return response.json() } -export function pushTranscations() { +//Can send multiple transactions in a push request +export async function pushTranscations(trans:Transaction[]):Promise{ + + const response = await fetch("api/transacitons", { + method: "POST", + headers: { + "content-type": "application/json", + "Cookie": document.cookie + }, + body: JSON.stringify(trans) + }) + + if(response.ok){ + alert("Updated/Create the transaction") + } else { + alert("Failed to edit/create transaction") + console.error(response.status) + } + return response.json() } \ No newline at end of file diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index 89a8146..24733a3 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' -import {postUserAccount, type Account} from '../../api/account.ts' +import {postUserAccount, type Account} from '../../api/Account.ts' import { validateAccountForm } from "../../util/ValidateForms.ts"; interface popupProp{ toggle: () => void; @@ -37,11 +37,19 @@ export default function popupForm({toggle, edit}:popupProp,){ //Check account before validating b/c account can be undefined //Determine if form input for an account is valid if(accountType && validateAccountForm(formInput.name, accountCategory[accountType], accountBalence,file,subtype,interest)) { - //Create a new account type and post it + const newAccount: Account = {id: 0, name:formInput.name, type: accountType, balance: accountBalence, subtype:"", value:0, last_updated: new Date()} console.log(newAccount) - postUserAccount(newAccount) - alert("Account " + formInput.name + " has been created") + + postUserAccount(newAccount).then((data)=>{ + if(data && data["id"]) + newAccount["id"] = data["id"] + + //Can't check both fields on same if statement, so another one is need to check + if(data["lastUpdated"]) + newAccount["last_updated"] = data["lastUpdated"] + }) + toggle() } else { alert("Please fill out the form") diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index 1d6740b..d6ec1dd 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -1,12 +1,14 @@ import React, { useState, type ReactHTMLElement } from "react"; import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' -import { getUserAccounts, type Account } from "../../api/account"; +import { getUserAccounts, type Account } from "../../api/Account"; import { validateTransactionForm } from "../../util/ValidateForms"; +import { pushTranscations, type Transaction } from "../../api/Transaction"; interface popupProp{ toggle: () => void; edit: boolean + selectedTransaction?: Transaction } @@ -22,20 +24,33 @@ export const transactionCategory = { type typeOfTransaction = keyof typeof transactionCategory //Returns a form of for the user to enter their info -export default function popupForm({toggle, edit}:popupProp){ +export default function popupForm({toggle, edit, selectedTransaction}:popupProp){ //Handles the submiting the form const handleSubmit = () =>{ + const accountId = Number(selectedAccount) const transferAmount = Number(amount) + let userTransaction: Transaction - if(selectedType && validateTransactionForm(Number(selectedAccount), selectedType, transferAmount, file)){ - - if(edit){ - alert("Transaction has been edited") - } else { - alert("Transaction has been created") - } + if(selectedType && validateTransactionForm(accountId, selectedType, transferAmount, file)){ + + userTransaction = {id: 0, financialAccount_id:accountId, amount:transferAmount, type: selectedType, date: new Date()} + + if(edit && selectedTransaction) { + + userTransaction["id"] = selectedTransaction["id"] + } + + pushTranscations([userTransaction]).then((data) => {data[0] + + //Successful push if data is returned + if(data && data[0]["id"]) { + userTransaction["id"] = data[0]["id"] + } + }) + + //Need to insert function that sets the state in parent component toggle() } else { @@ -79,8 +94,6 @@ export default function popupForm({toggle, edit}:popupProp){ } - //Gets user accounts - let userAccounts: Account[] = [] //Add when getUserAccounts is implemented getUserAccounts().then(accounts => { @@ -104,6 +117,11 @@ export default function popupForm({toggle, edit}:popupProp){ //Holds the types of transfers const transCat: typeOfTransaction [] = Object.keys(transactionCategory) as typeOfTransaction[]; + if(edit && selectedTransaction){ + setSelectedAccount(selectedTransaction.id.toString()) + setSelectedType(selectedTransaction.type) + setAmount(selectedTransaction.amount.toString()) + } return( @@ -115,7 +133,7 @@ export default function popupForm({toggle, edit}:popupProp){

@@ -131,7 +149,12 @@ export default function popupForm({toggle, edit}:popupProp){

- + {edit ? (null):( + <> + + + )} +

diff --git a/app/client/src/type/responseTypes.ts b/app/client/src/type/responseTypes.ts new file mode 100644 index 0000000..b14dfc4 --- /dev/null +++ b/app/client/src/type/responseTypes.ts @@ -0,0 +1,5 @@ +// +export interface updateResponse { + lastUpdated?: Date + id?: number +} \ No newline at end of file From e77de9cee2be2a7d58a028e975b29f91c1dd099b Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 23 Feb 2026 13:32:03 -0600 Subject: [PATCH 083/204] env file update --- app/server/.env | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/server/.env b/app/server/.env index 9db0df0..aa3d906 100644 --- a/app/server/.env +++ b/app/server/.env @@ -1,6 +1,13 @@ +#THIS IS NOT .gitignored'D! +#DON'T PUT REAL SECRETS IN HERE!!! + +#CI DOCKER_HUB_USERNAME=measureonecodetwice -RELEASE_VERSION=0 +RELEASE_VERSION=0.0.1 + +#DEPLOYMENT MYSQL_ROOT_PASSWORD=dummypw MYSQL_PASSWORD=dummypw MYSQL_USER=finus_app + JWT_SECRET=dummysecret From 49a36b6d6cf245f8bf3d823d14d92bbebabf559c Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 23 Feb 2026 13:51:04 -0600 Subject: [PATCH 084/204] added publish backend workflow --- .github/workflows/publish_backend_images.yml | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/publish_backend_images.yml diff --git a/.github/workflows/publish_backend_images.yml b/.github/workflows/publish_backend_images.yml new file mode 100644 index 0000000..e4b4e00 --- /dev/null +++ b/.github/workflows/publish_backend_images.yml @@ -0,0 +1,23 @@ +name: publishBackendImages + +on: + push: + branches: [ "release/**" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: setup + run: | + cd app/server + rm .env + BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + RELEASE_VERSION=echo $BRANCH_NAME | sed "s|release\/||g" + - name: build + run: docker compose build + - name: publish + run: docker compose push From 425f3a9aba234d92c58295e808f47d873e5a81a0 Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 23 Feb 2026 14:06:47 -0600 Subject: [PATCH 085/204] github workflow checkout: @v4 -> commit SHA --- .github/workflows/publish_backend_images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish_backend_images.yml b/.github/workflows/publish_backend_images.yml index e4b4e00..97f3f3f 100644 --- a/.github/workflows/publish_backend_images.yml +++ b/.github/workflows/publish_backend_images.yml @@ -10,7 +10,7 @@ jobs: publish: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: setup run: | cd app/server From ccf88b367574125f09a04ea4796b7250133ad39b Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 23 Feb 2026 14:56:37 -0600 Subject: [PATCH 086/204] fix CI issues --- .github/workflows/publish_backend_images.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish_backend_images.yml b/.github/workflows/publish_backend_images.yml index 97f3f3f..d37dadf 100644 --- a/.github/workflows/publish_backend_images.yml +++ b/.github/workflows/publish_backend_images.yml @@ -1,4 +1,4 @@ -name: publishBackendImages +name: "Publish Backend Images" on: push: @@ -9,15 +9,19 @@ on: jobs: publish: runs-on: ubuntu-24.04 + environment: "Docker hub" + defaults: + run: + working-directory: app/server steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: setup run: | - cd app/server - rm .env BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} - RELEASE_VERSION=echo $BRANCH_NAME | sed "s|release\/||g" - - name: build - run: docker compose build - - name: publish - run: docker compose push + RELEASE_VERSION=$(echo $BRANCH_NAME | sed "s|release\/||g") + echo RELEASE_VERSION=$RELEASE_VERSION > .env + echo DOCKER_HUB_USERNAME=${{ vars.DOCKER_HUB_USERNAME }} >> .env + + echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ vars.DOCKER_HUB_USERNAME }} --password-stdin + docker compose build -q + docker compose push -q From e700dfb634a3dfffd62a994f3e423ff3cf2e8bfb Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 23 Feb 2026 15:53:51 -0600 Subject: [PATCH 087/204] removed unused password files. Everything is in .env --- app/server/db_password.txt | 1 - app/server/db_root_password.txt | 1 - 2 files changed, 2 deletions(-) delete mode 100644 app/server/db_password.txt delete mode 100644 app/server/db_root_password.txt diff --git a/app/server/db_password.txt b/app/server/db_password.txt deleted file mode 100644 index 7aa311a..0000000 --- a/app/server/db_password.txt +++ /dev/null @@ -1 +0,0 @@ -password \ No newline at end of file diff --git a/app/server/db_root_password.txt b/app/server/db_root_password.txt deleted file mode 100644 index 2e65efe..0000000 --- a/app/server/db_root_password.txt +++ /dev/null @@ -1 +0,0 @@ -a \ No newline at end of file From 0b0ea7c4320ea142043adc9316724538c24efeb9 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 23 Feb 2026 15:59:38 -0600 Subject: [PATCH 088/204] DB: added a python populator script --- app/server/db_password.txt | 2 +- app/server/db_root_password.txt | 2 +- app/server/services/database/populator.py | 374 ++++++++++++++++++++++ app/server/services/database/schema.sql | 5 +- 4 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 app/server/services/database/populator.py diff --git a/app/server/db_password.txt b/app/server/db_password.txt index f3097ab..7aa311a 100644 --- a/app/server/db_password.txt +++ b/app/server/db_password.txt @@ -1 +1 @@ -password +password \ No newline at end of file diff --git a/app/server/db_root_password.txt b/app/server/db_root_password.txt index 7898192..2e65efe 100644 --- a/app/server/db_root_password.txt +++ b/app/server/db_root_password.txt @@ -1 +1 @@ -a +a \ No newline at end of file diff --git a/app/server/services/database/populator.py b/app/server/services/database/populator.py new file mode 100644 index 0000000..356fd93 --- /dev/null +++ b/app/server/services/database/populator.py @@ -0,0 +1,374 @@ +import mysql.connector +from mysql.connector import Error +import random +import hashlib +import os +from datetime import datetime, timedelta +import argparse +import subprocess + +# Database connection configuration +DB_CONFIG = { + 'host': '127.0.0.1', + 'port': 3306, + 'user': 'root', + 'password': 'a', + 'database': 'finus' +} + +# Configuration for mock data generation +NUM_USERS = 1 +ACCOUNTS_PER_USER = 3 # Average number of financial accounts per user +TRANSACTIONS_PER_ACCOUNT = 5 +START_DATE = datetime.now() - timedelta(days=365) + + + +def get_db_connection(): + try: + connection = mysql.connector.connect(**DB_CONFIG) + return connection + except Error as e: + print(f"Error connecting to db: {e}") + return None + +#need a special connection approach as this db is accessible through docker environment only +def execute_sql_via_docker(sql_commands): + process = subprocess.Popen( + ['docker', 'exec', '-i', 'server-database-1', 'mysql', '-u', 'root', '-pa'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + stdout, stderr = process.communicate(sql_commands) + if process.returncode != 0: + print(f"Error executing SQL: {stderr}") + return False + return True + + + +# USER_ACCOUNT = {'username': 'masterfulInvestor', 'email': '2lE1n@example.com', 'first_name': 'John', 'last_name': 'Finus', 'age': 30, 'created' : datetime.now()} +# CREDENTIALS = {'pw_hash': hashlib.sha256('password123'.encode()).hexdigest(), 'salt': os.urandom(16).hex()} +# PROFILE = {'name': 'John Finus Main', 'description': 'I love money'} +# GOAL = {'name': 'Buy a house','type': 'savings', 'description': 'Saving for a down payment on a house', 'objective': 'Save $500000.00', 'deadline': datetime.now() + timedelta(days=365*5)} +# ACCOUNT_CREDIT = {'name': 'Visa Credit Card', 'type': 'credit card', 'balance': -5000.00, 'value': 0, 'last_updated': datetime.now()} +# ACCOUNT_CHEQUING = {'name': 'Main Chequing', 'type': 'checking', 'balance': 15000.00, 'value': 15000.00, 'last_updated': datetime.now()} +# ACCOUNT_TFSA = {'name': 'TFSA Investment', 'type': 'investment', 'balance': 20000.00, 'value': 25000.00, 'last_updated': datetime.now()} +# ACCOUNT_RRSP = {'name': 'RRSP Savings', 'type': 'savings', 'balance': 30000.00, 'value': 35000.00, 'last_updated': datetime.now()} + +# TRANSACTIONS = [ +# {'amount': -150.00, 'date': datetime.now() - timedelta(days=10), 'description': 'Grocery Store', 'category': 'food', 'sender': 'Main Chequing', 'receiver': 'Grocery Store A'}, +# {'amount': -30.00, 'date': datetime.now() - timedelta(days=10), 'description': 'Grocery Store', 'category': 'food', 'sender': 'Visa Credit Card', 'receiver': 'Grocery Store A'}, +# {'amount': -200.00, 'date': datetime.now() - timedelta(days=5), 'description': 'Gas Station', 'category': 'fuel', 'sender': 'Main Chequing', 'receiver': 'Gas Station B'} +# ] + + +FINANCIAL_ACCOUNT_TYPES = ['chequing', 'savings', 'credit card', 'investment'] +FINANCIAL_ACCOUNT_SUBTYPES = ['personal', 'business', 'joint'] +INVESTMENT_TYPES = ['stocks', 'bonds', 'mutual funds', 'ETFs'] +GOAL_TYPES = ['money', 'debt'] +FIRST_NAMES = ['John', 'Jane', 'Alex', 'Emily', 'Michael', 'Sarah', 'David', 'Laura'] +LAST_NAMES = ['Smith', 'Finus', 'Williams', 'Brown', 'Jones'] +GOAL_NAMES = ['Emergency Fund', 'New Car', 'House Down Payment', 'Vacation', 'Retirement', 'Pay off Credit Card'] +GOAL_OBJECTIVES = ['Save $10,000', 'Save $20,000', 'Save $50,000', 'Save $5,000', 'Save $1,000,000', 'Pay off $5,000 debt'] +ACCOUNT_NAMES = ['Main Chequing', 'Savings Account', 'Visa Credit Card', 'TFSA Investment', 'RRSP Savings'] +TRANSACTION_DESCRIPTIONS = [ 'Grocery Store', 'Restaurant', 'Gas Station', 'Salary', 'Online Purchase', 'Rent Payment', 'Utility Bill', 'Phone Bill', 'Transfer', 'ATM Withdrawal', 'Streaming Subscription'] +SENDERS_RECIPIENTS = ['Employer Inc.', 'Supermarket Co.', 'Utility Corp', 'Friend', 'Family Member'] + +def hash_password(password): + salt = os.urandom(16) + pw_hash = hashlib.sha256((password + salt.hex()).encode()).hexdigest() + return pw_hash, salt.hex() + + +def create_goals(cursor, profile_ids): + for profile_id in profile_ids: + # create 1-3 goals per profile + for _ in range(random.randint(1, 3)): + name = random.choice(GOAL_NAMES) + goal_type = random.choice(GOAL_TYPES) + objective = random.choice(GOAL_OBJECTIVES) + description = f"Goal: {objective}" + deadline = datetime.now() + timedelta(days=random.randint(30, 365*3)) # 1 month to 3 years + + cursor.execute(""" + INSERT INTO finus.goal (name, type, objective, description, deadline) + VALUES (%s, %s, %s, %s, %s) + """, (name, goal_type, objective, description, deadline)) + + goal_id = cursor.lastrowid + + cursor.execute(""" + INSERT INTO finus.profile_goal (profile_id, goal_id) + VALUES (%s, %s) + """, (profile_id, goal_id)) + + + +def create_financial_accounts(cursor, profile_ids): + account_ids = [] + + for profile_id in profile_ids: + # create 2-5 accounts per profile + for _ in range(random.randint(2, 5)): + account_id = random.randint(10000, 99999) + while account_id in account_ids: + account_id = random.randint(10000, 99999) + + name = random.choice(ACCOUNT_NAMES) + acc_type = random.choice(FINANCIAL_ACCOUNT_TYPES) + balance = random.randint(-5000, 50000) + value = balance # for simplicity - may actually be different in real life + last_updated = datetime.now() + + # randomly assign subtype for savings accounts + subtype = None + if acc_type == 'savings' and random.random() > 0.5: + subtype = random.choice(FINANCIAL_ACCOUNT_SUBTYPES) + + cursor.execute(""" + INSERT INTO finus.financialAccount (id, name, type, balance, value, last_updated, subtype) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, (account_id, name, acc_type, balance, value, last_updated, subtype)) + + account_ids.append(account_id) + + cursor.execute(""" + INSERT INTO finus.finusAccount_profile (profile_id, account_id) + VALUES (%s, %s) + """, (profile_id, account_id)) + + return account_ids + + +def create_users_and_profiles(cursor): + user_ids = [] + profile_ids = [] + + for i in range(NUM_USERS): + first_name = random.choice(FIRST_NAMES) + last_name = random.choice(LAST_NAMES) + username = f"{first_name.lower()}.{last_name.lower()}{random.randint(1, 99)}" + email = f"{username}@example.com" + age = random.randint(18, 75) + + cursor.execute(""" + INSERT INTO finus.finusAccount + (username, email, first_name, last_name, age) + VALUES (%s, %s, %s, %s, %s) + """, (username, email, first_name, last_name, age)) + + user_id = cursor.lastrowid + user_ids.append(user_id) + + pw_hash, salt = hash_password("password123") + cursor.execute(""" + INSERT INTO finus.credentials (finus_account_id, pw_hash, salt) + VALUES (%s, %s, %s) + """, (user_id, pw_hash, salt)) + + # profile for each user + profile_name = f"{first_name}'s Profile" + profile_desc = f"Main profile for {first_name} {last_name}" + cursor.execute(""" + INSERT INTO finus.profile (name, description) + VALUES (%s, %s) + """, (profile_name, profile_desc)) + + profile_id = cursor.lastrowid + profile_ids.append(profile_id) + + cursor.execute(""" + INSERT INTO finus.finusAccount_profile (profile_id, account_id) + VALUES (%s, %s) + """, (profile_id, user_id)) + + return user_ids, profile_ids + + + +def create_transactions(cursor, account_ids): + """Create transactions for each financial account""" + print(f"Creating transactions (about {len(account_ids) * TRANSACTIONS_PER_ACCOUNT} total)...") + + for account_id in account_ids: + for _ in range(random.randint(10, 30)): # variable number of transactions + amount = random.randint(-500, 5000) + # ensure amount isn't 0 as that is weird + while amount == 0: + amount = random.randint(-500, 5000) + + description = random.choice(TRANSACTION_DESCRIPTIONS) + sender = random.choice(SENDERS_RECIPIENTS) if amount < 0 else None + recipient = random.choice(SENDERS_RECIPIENTS) if amount > 0 else None + + # random date within the last year + days_offset = random.randint(0, 365) + transaction_date = START_DATE + timedelta(days=days_offset) + + cursor.execute(""" + INSERT INTO finus.transaction + (financialAccount_id, amount, description, sender, recipient, date) + VALUES (%s, %s, %s, %s, %s, %s) + """, (account_id, amount, description, sender, recipient, transaction_date)) + + + +def create_investments(cursor, account_ids): + + for account_id in random.sample(account_ids, min(len(account_ids) // 2, 5)): # Up to 5 accounts + # create 1-3 investments per account + for _ in range(random.randint(1, 3)): + inv_type = random.choice(INVESTMENT_TYPES) + name = f"Investment {random.randint(1000, 9999)}" + description = f"A {inv_type} investment" + + cursor.execute(""" + INSERT INTO finus.investment (account_id, type, name, description) + VALUES (%s, %s, %s, %s) + """, (account_id, inv_type, name, description)) + + investment_id = cursor.lastrowid + + # create investment state + quantity = random.uniform(1, 100) + total_cost = quantity * random.uniform(10, 100) + + cursor.execute(""" + INSERT INTO finus.investmentState (investment_id, quantity, total_cost, at) + VALUES (%s, %s, %s, %s) + """, (investment_id, quantity, total_cost, datetime.now())) + + # add subtype-specific data + if inv_type == 'stock': + symbols = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA'] + symbol = random.choice(symbols) + cursor.execute(""" + INSERT INTO finus.stockInvestment (investment_id, symbol) + VALUES (%s, %s) + """, (investment_id, symbol)) + + elif inv_type == 'fixedInterest': + rate = random.uniform(0.01, 0.08) # 1-8% interest + frequency_years = random.choice([0, 1, 2, 3, 5]) + frequency_months = random.choice([0, 1, 3, 6]) if frequency_years == 0 else 0 + + cursor.execute(""" + INSERT INTO finus.fixedInterestInvestment + (investment_id, rate, frequency_years, frequency_months) + VALUES (%s, %s, %s, %s) + """, (investment_id, rate, frequency_years, frequency_months)) + + + +def clear_database(cursor): + """Clear all data from tables (in correct order due to foreign keys)""" + print("Clearing existing data...") + + tables_to_clear = [ + 'transaction', + 'stockInvestment', + 'fixedInterestInvestment', + 'investmentState', + 'investment', + 'finusAccount_profile', + 'goal', + 'profile_goal', + 'financialAccount', + 'credentials', + 'finusAccount', + 'profile' + ] + + for table in tables_to_clear: + cursor.execute(f"DELETE FROM finus.{table}") + + cursor.execute("ALTER TABLE finus.finusAccount AUTO_INCREMENT = 1") + cursor.execute("ALTER TABLE finus.profile AUTO_INCREMENT = 1") + cursor.execute("ALTER TABLE finus.goal AUTO_INCREMENT = 1") + cursor.execute("ALTER TABLE finus.transaction AUTO_INCREMENT = 1") + cursor.execute("ALTER TABLE finus.investment AUTO_INCREMENT = 1") + + + +def populate_lookup_tables(cursor): + + for acc_type in FINANCIAL_ACCOUNT_TYPES: + cursor.execute( + "INSERT IGNORE INTO finus.financialAccountType (type) VALUES (%s)", + (acc_type,) + ) + + for subtype in FINANCIAL_ACCOUNT_SUBTYPES: + cursor.execute( + "INSERT IGNORE INTO finus.financialAccountSubtype (type) VALUES (%s)", + (subtype,) + ) + + for inv_type in INVESTMENT_TYPES: + cursor.execute( + "INSERT IGNORE INTO finus.investmentType (type) VALUES (%s)", + (inv_type,) + ) + + for goal_type in GOAL_TYPES: + cursor.execute( + "INSERT IGNORE INTO finus.goalType (type) VALUES (%s)", + (goal_type,) + ) + + + +if __name__ == "__main__": + + # init_commands = """ + # CREATE DATABASE IF NOT EXISTS finus; + # USE finus; + # """ + # execute_sql_via_docker(init_commands) + + # get database connection + connection = get_db_connection() + if not connection: + print("Failed to connect to database") + exit(1) + + parser = argparse.ArgumentParser(description='Populate Finus database with mock data') + parser.add_argument('--clear', action='store_true', help='Clear existing data before populating') + parser.add_argument('--users', type=int, default=NUM_USERS, help=f'Number of users to create (default: {NUM_USERS})') + args = parser.parse_args() + + + + try: + cursor = connection.cursor() + + # clear existing data if requested + if args.clear: + clear_database(cursor) + # populate lookup tables + populate_lookup_tables(cursor) + + # Create main data + user_ids, profile_ids = create_users_and_profiles(cursor) + create_goals(cursor, profile_ids) + account_ids = create_financial_accounts(cursor, profile_ids) + create_transactions(cursor, account_ids) + create_investments(cursor, account_ids) + + # commit all changes + connection.commit() + print(f" Created: {len(user_ids)} users") + print(f" Created: {len(profile_ids)} profiles") + print(f" Created: {len(account_ids)} financial accounts") + print(f" Created: ~{len(account_ids) * TRANSACTIONS_PER_ACCOUNT} transactions") + + except Error as e: + print(f"Error during database population: {e}") + connection.rollback() + finally: + if connection.is_connected(): + cursor.close() + connection.close() \ No newline at end of file diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql index 274e7bb..fb21d5c 100644 --- a/app/server/services/database/schema.sql +++ b/app/server/services/database/schema.sql @@ -91,6 +91,7 @@ CREATE TABLE finus.transaction ( id INTEGER NOT NULL AUTO_INCREMENT, financialAccount_id INTEGER NOT NULL, amount INTEGER NOT NULL, + category VARCHAR(50) NOT NULL, -- Had to add this for analytics description VARCHAR(500), sender VARCHAR(50), recipient VARCHAR(50), @@ -151,7 +152,7 @@ CREATE TABLE finus.fixedInterestInvestment( ); #Populate lookup tables -#INSERT INTO finus.financialAccountType (type) VALUES (); -#INSERT INTO finus.financialAccountSubtype (type) VALUES (); +INSERT INTO finus.financialAccountType (type) VALUES ('chequing'), ('savings'), ('credit_card'), ('investment'); +INSERT INTO finus.financialAccountSubtype (type) VALUES ('RRSP'), ('TFSA'), ('FHSA'), ('RESP'), ('RDSP'); INSERT INTO finus.investmentType (type) VALUES ('fixedInterest'), ('stock'); #These have to match table names INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); From a2407e25ddea841d51fb4b8c24821bb2af40a4c1 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 23 Feb 2026 18:09:56 -0600 Subject: [PATCH 089/204] Client: rework of the dashboard page to work with updated auth feature --- app/client/src/pages/DashboardPage.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 37185dc..fc9bcc4 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -10,17 +10,24 @@ import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartDat import type { SankeyData } from 'recharts/types/chart/Sankey'; import ChartSection from '@/components/DashboardChartSection'; import DashboardChartSection from '@/components/DashboardChartSection'; +import type { AuthSession } from './authTypes'; Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); +type DashboardPageProps = { + session: AuthSession; + onLogout: () => void; +}; - -function DashboardPage() { +function DashboardPage({ session, onLogout }: DashboardPageProps) { return (
-

Welcome Username

+ +

Hello {session.user.first_name ?? session.user.name ?? "there"}

Here you can view your recent transactions and manage your finances.

From 71d00bae6e7ba57b321fe1b2ee57df3e8ea3f914 Mon Sep 17 00:00:00 2001 From: Huy Truong Date: Mon, 23 Feb 2026 18:47:11 -0600 Subject: [PATCH 090/204] Client: Keep refactoring --- app/client/src/api/ManagerAPI.tsx | 124 ++++++++++++++++-- .../src/components/DashboardChartSection.tsx | 123 ++++++++--------- .../src/components/TransactionTable.tsx | 109 +++------------ app/client/src/pages/DashboardPage.tsx | 6 +- 4 files changed, 191 insertions(+), 171 deletions(-) diff --git a/app/client/src/api/ManagerAPI.tsx b/app/client/src/api/ManagerAPI.tsx index e1b681f..c77c546 100644 --- a/app/client/src/api/ManagerAPI.tsx +++ b/app/client/src/api/ManagerAPI.tsx @@ -3,20 +3,116 @@ import { BASE_URL } from "@/utils/constants"; import type { ChartData } from "chart.js"; import type { SankeyData } from 'recharts/types/chart/Sankey'; import type React from "react"; -// import type { Transaction } from "@/types/Transaction"; +import type { Transaction } from "@/types/Transaction"; -// async function getTransactions(): Promise { -// try { -// const response = await axios.get(`${BASE_URL}/api/transactions`); -// if (response.status !== 200) { -// throw new Error(`Failed to fetch transactions: ${response.statusText}`); -// } -// return response.data; -// } catch (error) { -// console.error("Error fetching transactions:", error); -// throw error; -// } -// } +async function getTransactions(): Promise { + /* Uncomment this block once backend is ready to test fetching transactions */ + // try { + // const response = await axios.get(`${BASE_URL}/api/transactions`); + // if (response.status !== 200) { + // throw new Error(`Failed to fetch transactions: ${response.statusText}`); + // } + // return response.data; + // } catch (error) { + // console.error("Error fetching transactions:", error); + // throw error; + // } + + //Assume doing api call here and getting transactions back, but for now just simulate with timeout + const transactions: Transaction[] = [ + { + id: "tx001", + amount: 1200, + category: "Income", + date: "2026-02-01", + from: "Tech Corp Inc.", + to: "Huy Truong", + description: "Monthly salary payment" + }, + { + id: "tx002", + amount: -75.5, + category: "Expenses", + date: "2026-02-02", + from: "Huy Truong", + to: "Walmart", + description: "Weekly grocery shopping" + }, + { + id: "tx003", + amount: -45, + category: "Expenses", + date: "2026-02-03", + from: "Huy Truong", + to: "City Bus Service", + description: "Monthly bus pass" + }, + { + id: "tx004", + amount: -120, + category: "Expenses", + date: "2026-02-04", + from: "Huy Truong", + to: "Hydro Company", + description: "Electricity bill" + }, + { + id: "tx005", + amount: -60, + category: "Expenses", + date: "2026-02-05", + from: "Huy Truong", + to: "Rogers", + description: "Home internet bill" + }, + { + id: "tx006", + amount: -35.75, + category: "Expenses", + date: "2026-02-06", + from: "Huy Truong", + to: "Tim Hortons", + description: "Lunch with friends" + }, + { + id: "tx007", + amount: -15.99, + category: "Expenses", + date: "2026-02-07", + from: "Huy Truong", + to: "Netflix", + description: "Monthly subscription" + }, + { + id: "tx008", + amount: -200, + category: "Expenses", + date: "2026-02-08", + from: "Huy Truong", + to: "Amazon", + description: "Electronics purchase" + }, + { + id: "tx009", + amount: 150, + category: "Expenses", + date: "2026-02-09", + from: "Client ABC", + to: "Huy Truong", + description: "Website development payment" + }, + { + id: "tx010", + amount: -500, + category: "Expenses", + date: "2026-02-01", + from: "Huy Truong", + to: "Landlord", + description: "Monthly rent" + } + ]; + return new Promise(resolve => setTimeout(() => resolve(transactions), 1000)); +} @@ -114,7 +210,7 @@ async function getIncomeFlowChartData(period: string): Promise { export { - // getTransactions, + getTransactions, getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData diff --git a/app/client/src/components/DashboardChartSection.tsx b/app/client/src/components/DashboardChartSection.tsx index 6b6d8e2..944aa1a 100644 --- a/app/client/src/components/DashboardChartSection.tsx +++ b/app/client/src/components/DashboardChartSection.tsx @@ -19,50 +19,6 @@ function DashboardChartSection() { const [incomeData, setIncomeData] = useState(null); const [isLoading, setIsLoading] = useState(false); - const data = { - labels: [ - 'Red', - 'Blue', - 'Yellow' - ], - datasets: [{ - label: 'My First Dataset', - data: [300, 50, 100], - backgroundColor: [ - 'rgb(255, 99, 132)', - 'rgb(54, 162, 235)', - 'rgb(255, 205, 86)' - ], - hoverOffset: 20, - circumference: 360, - }] - }; - - - const options = { - responsive: true, - plugins: { - legend: { - position: 'right' as const, - }, - title: { - display: true, - text: 'Expenses Breakdown', - font: { - size: 24, - weight: 'bold' as const - } - }, - layout: { - padding: 500, - - }, - legendDistance: { - padding: 50 - } - - }, - }; const fetchExpensesData = async () => { setIsLoading(true); @@ -221,9 +177,9 @@ const savingsLineOptions = { ); } return ( -
+ -
+ ); case 'savings': if (isLoading || !savingsData) { @@ -234,9 +190,9 @@ const savingsLineOptions = { ); } return ( -
+ -
+ ); case 'income': if (isLoading || !incomeData) { @@ -247,7 +203,7 @@ const savingsLineOptions = { ); } return ( -
+
); @@ -262,25 +218,72 @@ const savingsLineOptions = { }; return ( <> -
- - - +
+ + +
-
-
+
+
{/* This chart is here just to test all the graph components */} - - - +
+ + + +
{renderChart()} -
+ {/*
-
+
*/}
) } + /*const data = { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 20, + circumference: 360, + }] + }; + + + const options = { + responsive: true, + plugins: { + legend: { + position: 'right' as const, + }, + title: { + display: true, + text: 'Expenses Breakdown', + font: { + size: 24, + weight: 'bold' as const + } + }, + layout: { + padding: 500, + + }, + legendDistance: { + padding: 50 + } + + }, + };*/ + export default DashboardChartSection \ No newline at end of file diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index 278c5f7..b473e02 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -1,99 +1,22 @@ +import { getTransactions } from '@/api/ManagerAPI'; import type { Transaction } from '@/types/Transaction'; -import React from 'react' +import React, { useEffect, useState } from 'react' + -const transactions: Transaction[] = [ - { - id: "tx001", - amount: 1200, - category: "Income", - date: "2026-02-01", - from: "Tech Corp Inc.", - to: "Huy Truong", - description: "Monthly salary payment" - }, - { - id: "tx002", - amount: -75.5, - category: "Expenses", - date: "2026-02-02", - from: "Huy Truong", - to: "Walmart", - description: "Weekly grocery shopping" - }, - { - id: "tx003", - amount: -45, - category: "Expenses", - date: "2026-02-03", - from: "Huy Truong", - to: "City Bus Service", - description: "Monthly bus pass" - }, - { - id: "tx004", - amount: -120, - category: "Expenses", - date: "2026-02-04", - from: "Huy Truong", - to: "Hydro Company", - description: "Electricity bill" - }, - { - id: "tx005", - amount: -60, - category: "Expenses", - date: "2026-02-05", - from: "Huy Truong", - to: "Rogers", - description: "Home internet bill" - }, - { - id: "tx006", - amount: -35.75, - category: "Expenses", - date: "2026-02-06", - from: "Huy Truong", - to: "Tim Hortons", - description: "Lunch with friends" - }, - { - id: "tx007", - amount: -15.99, - category: "Expenses", - date: "2026-02-07", - from: "Huy Truong", - to: "Netflix", - description: "Monthly subscription" - }, - { - id: "tx008", - amount: -200, - category: "Expenses", - date: "2026-02-08", - from: "Huy Truong", - to: "Amazon", - description: "Electronics purchase" - }, - { - id: "tx009", - amount: 150, - category: "Expenses", - date: "2026-02-09", - from: "Client ABC", - to: "Huy Truong", - description: "Website development payment" - }, - { - id: "tx010", - amount: -500, - category: "Expenses", - date: "2026-02-01", - from: "Huy Truong", - to: "Landlord", - description: "Monthly rent" - } -]; function TransactionTable() { + const [transactions, setTransactions] = useState([]); + + useEffect(() => { + const fetchTransactions = async () => { + try { + const txs = await getTransactions(); + setTransactions(txs); + } catch (error) { + console.error("Error fetching transactions:", error); + } + }; + fetchTransactions(); + },[]) return (
{/* Header */} diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 37185dc..f2308d9 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -6,7 +6,7 @@ import AccountCard from '@/components/AccountCard'; import TransactionTable from '@/components/TransactionTable'; import LoadingSpinner from '@/components/LoadingSpinner'; import SankeyChart from '@/components/SankeyChart'; -import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; +import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData, getTransactions } from '@/api/ManagerAPI'; import type { SankeyData } from 'recharts/types/chart/Sankey'; import ChartSection from '@/components/DashboardChartSection'; import DashboardChartSection from '@/components/DashboardChartSection'; @@ -15,8 +15,6 @@ Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale function DashboardPage() { - - return (
@@ -31,7 +29,7 @@ function DashboardPage() {

Recent Transactions

- +
) } From 95ad0ca4722dc105fe9d6901deec43eec1d117b6 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 23 Feb 2026 21:05:33 -0600 Subject: [PATCH 091/204] Merge: cleaned up post-merge of dev into visualization, populator works at this point --- .../services/database/grantPrivileges.sh | 19 ++- app/server/services/database/populator.py | 119 ++++++++---------- app/server/services/database/schema.sql | 24 ++-- 3 files changed, 86 insertions(+), 76 deletions(-) diff --git a/app/server/services/database/grantPrivileges.sh b/app/server/services/database/grantPrivileges.sh index 56b7e1c..85630dc 100755 --- a/app/server/services/database/grantPrivileges.sh +++ b/app/server/services/database/grantPrivileges.sh @@ -1,6 +1,19 @@ #!/bin/sh +#set -e +#mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e " +# GRANT ALL PRIVILEGES ON finus.* TO '$MYSQL_USER'@'%'; +# " + set -e -mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e " -L PRIVILEGES ON finus.* TO '$MYSQL_USER'@'%'; -" +mysql -u root -p"$MYSQL_ROOT_PASSWORD" < Date: Mon, 23 Feb 2026 21:39:43 -0600 Subject: [PATCH 092/204] Merge: integreated auth feature to work with the database --- app/server/services/database/populator.py | 2 +- app/server/services/ts/auth/src/index.ts | 4 +++- app/server/services/ts/auth/src/logic.ts | 8 ++++++-- app/server/services/ts/auth/src/queries.ts | 4 ++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/server/services/database/populator.py b/app/server/services/database/populator.py index 1a0f3a4..a53d421 100644 --- a/app/server/services/database/populator.py +++ b/app/server/services/database/populator.py @@ -18,7 +18,7 @@ DB_CONFIG = { 'host': '127.0.0.1', 'port': 3306, - 'user': 'root', + 'user': 'finus_app',#'root', 'password': 'dummypw', #very unsafe, yes, but this file should not be accessible in prod 'database': 'finus' } diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts index 36bb800..d2fbe3f 100644 --- a/app/server/services/ts/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -10,13 +10,14 @@ import { parseLoginBody, parseSignupBody } from "./parsing"; const pool = mysql.createPool({ host: process.env.MYSQL_HOST, port: Number(process.env.MYSQL_PORT), - user: process.env.MYSQL_USER, + user: 'root',//process.env.MYSQL_USER, // ------------------------------------ Needs fixing, finus_app gets denied access, this might be an issue of accessing the db from outside its container password: process.env.MYSQL_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, }); + const app = express(); app.use(express.json()); app.use(buildCorsConfig(undefined)); @@ -30,6 +31,7 @@ app.post("/signup", async (req, res) => { return; } + console.log("In Index, calling for signup function"); signup(body, res, pool); }); diff --git a/app/server/services/ts/auth/src/logic.ts b/app/server/services/ts/auth/src/logic.ts index abc6458..3ef29fd 100644 --- a/app/server/services/ts/auth/src/logic.ts +++ b/app/server/services/ts/auth/src/logic.ts @@ -27,15 +27,18 @@ export async function signup(body: SignupBody, res, pool) { return; } + console.log("Passed email, pwd and age checks, going to check for existing email in db"); + try { - const existingUser = await emailExists(body.email, pool); + //const existingUser = await emailExists(body.email, pool); + const existingUser = false;//debugging email check for now if (existingUser) { res.status(409).json({ error: "An account with this email already exists.", }); return; } - } catch { + } catch (error) { res.status(500).json({ error: "Could not validate account state." }); return; } @@ -44,6 +47,7 @@ export async function signup(body: SignupBody, res, pool) { algorithm: "bcrypt", }); try { + console.log("Inserting user into database..."); await insertUser({ ...body, pw_hash: passwordHash }, pool); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/app/server/services/ts/auth/src/queries.ts b/app/server/services/ts/auth/src/queries.ts index 9e5c843..28905b2 100644 --- a/app/server/services/ts/auth/src/queries.ts +++ b/app/server/services/ts/auth/src/queries.ts @@ -52,6 +52,8 @@ export async function insertUser(user: UserWithPassword, pool): void { [user.username, user.email, user.first_name, user.last_name, user.age], ); + console.log("Executed insert into finus account"); + await connection.execute( ` INSERT INTO credentials (finus_account_id, pw_hash) @@ -60,6 +62,8 @@ export async function insertUser(user: UserWithPassword, pool): void { [accountInsert.insertId, user.pw_hash], ); + console.log("User created with ID:", accountInsert.insertId); + await connection.commit(); connection.release(); } catch (error) { From 6a57cbf30c1db0be9e57f33817ce599a69fb4d51 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 23 Feb 2026 21:55:36 -0600 Subject: [PATCH 093/204] Visualization: fixed sankey chart render --- .../services/python/analytics/src/main.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py index b8e59f7..c3e40b6 100644 --- a/app/server/services/python/analytics/src/main.py +++ b/app/server/services/python/analytics/src/main.py @@ -5,6 +5,7 @@ import numpy as np import uvicorn import os +import mysql.connector as mysql app = FastAPI() @@ -16,6 +17,14 @@ allow_headers=["*"], ) +def get_db_connection(): + return mysql.connect( + host=os.getenv("DB_HOST", "localhost"), + user=os.getenv("DB_USER", "root"), + password=os.getenv("DB_PASSWORD", "dummypw"), + database=os.getenv("DB_NAME", "finus") + ) + # test endpoint @app.get('/health') @@ -48,6 +57,25 @@ async def get_savings(period: str): @app.get('/charts/incomeflow') async def get_income_flow(period: str = Query(default='w', enum=['w', 'm', 'y'])): + #fetch all positive and negative transactions for the user that fall within today and the start of the period (week, month, year) + #aggregate the transactions by category and sum the values for each category + #return the aggregated data as a list of dictionaries with the category as the key and the sum of the values as the value + + return get_income_flow_synth(period) + #TODO: replace with real data from db when available + connection = get_db_connection() + cursor = connection.cursor() + query = """SELECT category, SUM(value) as total_value + FROM transactions + WHERE user_id = %s AND date >= %s AND date <= %s + GROUP BY category""" + cursor.execute(query, (1, pd.Timestamp.today().strftime('%Y-%m-%d'), f'{pd.Timestamp.today().year}-{np.random.randint(1, 13):02d}-{np.random.randint(1, 29):02d}')) + results = cursor.fetchall() + connection.close() + + +@app.get('/charts/incomeflow-synth') +async def get_income_flow_synth(period: str = Query(default='w', enum=['w', 'm', 'y'])): NUM_R_D = 5 NUM_R_C = 10 From f7e31578bb1fe1b62d53466093023aa12ff7a99e Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Mon, 23 Feb 2026 21:57:11 -0600 Subject: [PATCH 094/204] Cleanup: got rid of old debugging code --- .../src/components/DashboardChartSection.tsx | 50 ++----------------- app/client/src/components/SankeyChart.tsx | 4 +- app/client/src/index.css | 4 +- app/client/src/pages/DashboardPage.tsx | 2 - 4 files changed, 8 insertions(+), 52 deletions(-) diff --git a/app/client/src/components/DashboardChartSection.tsx b/app/client/src/components/DashboardChartSection.tsx index 944aa1a..e9761ea 100644 --- a/app/client/src/components/DashboardChartSection.tsx +++ b/app/client/src/components/DashboardChartSection.tsx @@ -122,7 +122,7 @@ function DashboardChartSection() { }catch(error) { console.error("Error fetching income flow chart data:", error); } - console.log("Using placeholder income flow chart data"); + //console.log("Using placeholder income flow chart data"); return { nodes: [], links: [], @@ -203,9 +203,9 @@ const savingsLineOptions = { ); } return ( -
+ //
-
+ //
); default: return ( @@ -241,49 +241,5 @@ const savingsLineOptions = { ) } - /*const data = { - labels: [ - 'Red', - 'Blue', - 'Yellow' - ], - datasets: [{ - label: 'My First Dataset', - data: [300, 50, 100], - backgroundColor: [ - 'rgb(255, 99, 132)', - 'rgb(54, 162, 235)', - 'rgb(255, 205, 86)' - ], - hoverOffset: 20, - circumference: 360, - }] - }; - - - const options = { - responsive: true, - plugins: { - legend: { - position: 'right' as const, - }, - title: { - display: true, - text: 'Expenses Breakdown', - font: { - size: 24, - weight: 'bold' as const - } - }, - layout: { - padding: 500, - - }, - legendDistance: { - padding: 50 - } - - }, - };*/ export default DashboardChartSection \ No newline at end of file diff --git a/app/client/src/components/SankeyChart.tsx b/app/client/src/components/SankeyChart.tsx index 045fd0a..24142c2 100644 --- a/app/client/src/components/SankeyChart.tsx +++ b/app/client/src/components/SankeyChart.tsx @@ -39,8 +39,8 @@ function CustomNode({ x, y, width, height, index, payload }: SankeyNodeProps) { if (containerWidth == null) { return null; // return null if used outside a chart context } - console.log("width of container:", containerWidth) - console.log("x:", x, "width of node:", width); + //console.log("width of container:", containerWidth) + //console.log("x:", x, "width of node:", width); const isOut = x + width + TEXT_OFFSET > containerWidth-TEXT_OFFSET*3; return ( diff --git a/app/client/src/index.css b/app/client/src/index.css index 6af8f5f..674bce0 100644 --- a/app/client/src/index.css +++ b/app/client/src/index.css @@ -1,8 +1,10 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap'); + @custom-variant dark (&:is(.dark *)); -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap'); + :root { font-family: 'Space Grotesk', 'Segoe UI', Tahoma, sans-serif; diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 3240114..aebbaca 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -20,8 +20,6 @@ type DashboardPageProps = { function DashboardPage({ session, onLogout }: DashboardPageProps) { - - return (
- {seen ? ():null} + {seen ? ():null} ) diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index 07ec009..d313503 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -54,4 +54,53 @@ export async function postUserAccount(newAccount:Account):Promise { + + //Create post request and wait for response + const response = await fetch('/api/accounts', { + method: "PUT", + headers: { + 'content-type': '/application/json', + 'Cookie': document.cookie + }, + body: JSON.stringify(newAccount) + } ) + + //Determine if our post was a success + if(response.ok){ + alert("Account " + newAccount.name + " has been updated" ) + } else { + alert("Failed to update account " + newAccount.name) + console.error(response.status) + } + + return response.json() +} + + + +export async function deleteUserAccount(userAccount:Account) { + + const content = JSON.stringify({id:userAccount.id}) + //Create delete request to delete the account + const response = await fetch('api/accounts',{ + method: "DELETE", + headers: { + "content-type": "/application/json", + "Cookie":document.cookie + }, + body: content + }) + + if(response.ok){ + alert("Account " + userAccount.name + " has been deleted") + } else { + alert("Failed to delete account " + userAccount.name) + console.error(response.status) + } + + return response.ok } \ No newline at end of file diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts new file mode 100644 index 0000000..d339a27 --- /dev/null +++ b/app/client/src/api/Income.ts @@ -0,0 +1,62 @@ +import type { updateResponse } from "../type/responseTypes" + +export interface Income { + id:number, + name:string, + income: number, + description: string +} + +export async function getIncome() { + + const response = await fetch('/api/income', { + method: "GET", + headers: { + "Cookie": document.cookie + } + }) + + if(!response.ok) { + console.error(response.status) + alert("Failed to retrieve income") + } +} + +export async function postIncome(name:string, income:number, description:string): Promise{ + const response = await fetch("/api/income",{ + method: "POST", + headers:{ + "Cookie":document.cookie + }, + body: JSON.stringify({"name":name,"income":income,"description":description}) + }) + + if(response.ok){ + alert("Created income " + name) + } else { + alert("Failed to create income " + name) + console.error(response.status) + } + + return response.json() +} + + +export async function putIncome(name:string, income:number, description:string) { + const response = await fetch("/api/income",{ + method: "POST", + headers:{ + "Cookie":document.cookie + }, + body: JSON.stringify({"name":name,"income":income,"description":description}) + }) + + if(response.ok){ + alert("Created income " + name) + } else { + alert("Failed to create income " + name) + console.error(response.status) + } + + return response.ok +} \ No newline at end of file diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index b541aa8..9f76d55 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -43,17 +43,62 @@ export async function pushTranscations(trans:Transaction[]):Promise{ + + const response = await fetch("api/transacitons", { + method: "PUT", + headers: { + "content-type": "application/json", + "Cookie": document.cookie, + }, + body: JSON.stringify(trans) + }) + + if(response.ok){ + alert("Updated the transaction") + } else { + alert("Failed to update transaction") + console.error(response.status) + } + + return response.json() +} + +export async function deleteTransaction(selectedTransaction:Transaction) { + + const content = JSON.stringify({id:selectedTransaction.id, financialAccount_id:selectedTransaction.financialAccount_id}) + //Create delete request to delete the account + const response = await fetch('api/accounts',{ + method: "DELETE", + headers: { + "content-type": "/application/json", + "Cookie":document.cookie + }, + body: content + }) + + if(response.ok){ + alert("Transaction has been deleted") + } else { + alert("Failed to delete transaction") + console.error(response.status) + } + + return response.ok } \ No newline at end of file diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index 24733a3..0a99c1f 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -3,9 +3,13 @@ import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' import {postUserAccount, type Account} from '../../api/Account.ts' import { validateAccountForm } from "../../util/ValidateForms.ts"; +import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput.ts"; + interface popupProp{ toggle: () => void; + setAccount: (account:Account) => void edit: boolean + selectedAccount?: Account } export const accountCategory = { @@ -17,14 +21,14 @@ export const accountCategory = { type typeofAccount = keyof typeof accountCategory -export default function popupForm({toggle, edit}:popupProp,){ +export default function popupForm({toggle, setAccount, edit, selectedAccount}:popupProp){ //Submits the account data const handleSubmit = () => { let subtype = undefined let interest = undefined - const accountBalence = Number(balence) + const accountBalance = Number(balance) if(accountType === accountCategory.SAVING) { subtype = formInput.subType @@ -36,18 +40,21 @@ export default function popupForm({toggle, edit}:popupProp,){ //Check account before validating b/c account can be undefined //Determine if form input for an account is valid - if(accountType && validateAccountForm(formInput.name, accountCategory[accountType], accountBalence,file,subtype,interest)) { + if(accountType && validateAccountForm(formInput.name, accountCategory[accountType], accountBalance,file,subtype,interest)) { - const newAccount: Account = {id: 0, name:formInput.name, type: accountType, balance: accountBalence, subtype:"", value:0, last_updated: new Date()} + const newAccount: Account = {id: 0, name:formInput.name, type: accountType, balance: accountBalance, subtype:"", value:0, last_updated: new Date()} console.log(newAccount) + //Send a post request to create or edit the account postUserAccount(newAccount).then((data)=>{ + + //When creating new account, id is returned for that account if(data && data["id"]) newAccount["id"] = data["id"] - //Can't check both fields on same if statement, so another one is need to check - if(data["lastUpdated"]) - newAccount["last_updated"] = data["lastUpdated"] + + if(data["lastUpdated"]) + newAccount["last_updated"] = data["lastUpdated"] }) toggle() @@ -61,22 +68,6 @@ export default function popupForm({toggle, edit}:popupProp,){ setFormInput({...formInput, [event.target.name] : event.target.value}); } - //Handles state when currency is changed - const handleCurrencyChange = (event:React.ChangeEvent) => { - - let input = event.target.value - const pattern = /^\d*\.?\d{0,2}$/ - - console.log(input) - console.log(pattern.test(input)) - //Determine if the input follows the format/pattern - if(pattern.test(input) || input === ""){ - input = input.replace(/^0+(?=\d)/,"") - setBalence(input) - - } - } - //Handles the bank statement const handleFile = (event:React.ChangeEvent) => { @@ -87,16 +78,10 @@ export default function popupForm({toggle, edit}:popupProp,){ } } - const handleCurrencyBlur = (event:React.ChangeEvent) => { - if(event.target.value !== "") { - setBalence(parseFloat(balence).toFixed(2)) - } - - } const [formInput, setFormInput] = useState({name: '', subType:'', interest: 0}) const [file, setFile] = useState(undefined) - const[balence, setBalence] = useState("") + const[balance, setBalance] = useState("") const[accountType, setAccountType] = useState(undefined) const accountCat: typeofAccount[] = Object.keys(accountCategory) as typeofAccount[] @@ -121,7 +106,7 @@ export default function popupForm({toggle, edit}:popupProp,){

- + handleCurrencyChange(event, setBalance)} onBlur ={(event) => handleCurrencyBlur(event,balance,setBalance)} placeholder="0.00"/>

diff --git a/app/client/src/components/ui/IncomeForm.tsx b/app/client/src/components/ui/IncomeForm.tsx new file mode 100644 index 0000000..fda9515 --- /dev/null +++ b/app/client/src/components/ui/IncomeForm.tsx @@ -0,0 +1,79 @@ +import { useState } from "react" +import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput" +import './UserForm.css' +import {getIncome, postIncome, putIncome, type Income } from "../../api/Income" +import { validateAccountForm, validateIncomeForm } from "../../util/ValidateForms"; + +interface popupProp{ + toggle: () => void; + setIncome: (income:Income) => void + addIncome: (income:Income) => void + edit: boolean + selectedIncome?: Income +} + +export default function popupForm({toggle,setIncome, addIncome, edit, selectedIncome}:popupProp){ + + const handleSubmit = () => { + + //Convert to number + const inputAmount = Number(amount) + + if(validateIncomeForm(name, Number(amount))){ + + if(edit) { + putIncome(name,inputAmount,description).then((result) =>{ + if(result && selectedIncome) { + const updateIncome: Income = {id:selectedIncome.id, name:name, income:inputAmount, description:description} + setIncome(updateIncome) + } + }) + + } else { + postIncome(name,inputAmount,description).then((data)=>{ + + const newIncome: Income = {id: 0, name:name, income:inputAmount, description:description} + + //Determine if response gave back an id for the new created income + if(data) { + + if(data.id){ + + newIncome.id = data.id + addIncome(newIncome) + } + } + }) + } + } + } + + const [name, setName] = useState('') + const [amount, setAmount] = useState('') + const [description, setDescription] = useState('') + + return( +
+

Income

+

+ + + setName(event.target.value)}/> +

+ + + handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event,amount,setAmount)}/> +

+ + + setDescription(event.target.value)}/> +

+ +
+ + {edit? (): ()} +
+
+ + ) +} \ No newline at end of file diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index d6ec1dd..b4e1ae3 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -4,9 +4,11 @@ import './UserForm.css' import { getUserAccounts, type Account } from "../../api/Account"; import { validateTransactionForm } from "../../util/ValidateForms"; import { pushTranscations, type Transaction } from "../../api/Transaction"; +import { handleCurrencyChange } from "../../util/handleInput"; interface popupProp{ toggle: () => void; + setTransaction: () => void edit: boolean selectedTransaction?: Transaction } @@ -59,22 +61,6 @@ export default function popupForm({toggle, edit, selectedTransaction}:popupProp) } - //Handles state when currency is changed - const handleCurrencyChange = (event:React.ChangeEvent) => { - - let input = event.target.value - const pattern = /^\d*\.?\d{0,2}$/ - - console.log(input) - console.log(pattern.test(input)) - //Determine if the input follows the format/pattern - if(pattern.test(input) || input === ""){ - input = input.replace(/^0+(?=\d)/,"") - setAmount(input) - - } - } - const handleCurrencyBlur = (event:React.ChangeEvent) => { if(event.target.value !== "") { setAmount(parseFloat(amount).toFixed(2)) @@ -146,7 +132,7 @@ export default function popupForm({toggle, edit, selectedTransaction}:popupProp)

- + handleCurrencyChange(event, setAmount)} onBlur={handleCurrencyBlur} placeholder="0.00"/>

{edit ? (null):( diff --git a/app/client/src/util/ValidateForms.ts b/app/client/src/util/ValidateForms.ts index 2526044..8a06908 100644 --- a/app/client/src/util/ValidateForms.ts +++ b/app/client/src/util/ValidateForms.ts @@ -39,4 +39,8 @@ export function validateTransactionForm(account_id:number, transactionType:strin } return result -} \ No newline at end of file +} + +export function validateIncomeForm(name:string, income:number){ + return name && income > 0 +} \ No newline at end of file diff --git a/app/client/src/util/handleInput.ts b/app/client/src/util/handleInput.ts new file mode 100644 index 0000000..e87a2b7 --- /dev/null +++ b/app/client/src/util/handleInput.ts @@ -0,0 +1,23 @@ +import type React from "react" + +//Handles state when currency is changed +export const handleCurrencyChange = (event:React.ChangeEvent, setCurrency: React.Dispatch>): void => { + + let input = event.target.value + const pattern = /^\d*\.?\d{0,2}$/ + + console.log(input) + console.log(pattern.test(input)) + //Determine if the input follows the format/pattern + if(pattern.test(input) || input === ""){ + input = input.replace(/^0+(?=\d)/,"") + setCurrency(input) + + } +} + +export const handleCurrencyBlur = (event:React.ChangeEvent,currency:string, setCurrency: React.Dispatch>): void => { + if(event.target.value !== "") { + setCurrency(parseFloat(currency).toFixed(2)) + } +} \ No newline at end of file From d8a1c84cdc3253e4690178ca0395d4ce7a040357 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 27 Feb 2026 04:45:19 -0600 Subject: [PATCH 103/204] Fixed the url for the fetches and added delete request for income --- app/client/src/api/Account.ts | 11 +++++++---- app/client/src/api/Income.ts | 32 ++++++++++++++++++++++++++++--- app/client/src/api/Transaction.ts | 13 ++++++++----- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index d313503..8da0608 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,3 +1,4 @@ +import { request } from "http"; import type { updateResponse } from "../type/responseTypes"; //Coppied from module @@ -11,11 +12,13 @@ export interface Account{ last_updated: Date; } +const requestUrl = 'http://localhost:3000/accounts' + //Sends a request to get different accounts the user has export async function getUserAccounts(): Promise { //Sends a http request and waits for a response - const response = await fetch('/api/accounts', { + const response = await fetch(requestUrl, { method: 'GET', headers: { 'Cookie': document.cookie @@ -36,7 +39,7 @@ export async function getUserAccounts(): Promise { export async function postUserAccount(newAccount:Account):Promise { //Create post request and wait for response - const response = await fetch('/api/accounts', { + const response = await fetch(requestUrl, { method: "POST", headers: { 'content-type': '/application/json', @@ -60,7 +63,7 @@ export async function postUserAccount(newAccount:Account):Promise { //Create post request and wait for response - const response = await fetch('/api/accounts', { + const response = await fetch(requestUrl, { method: "PUT", headers: { 'content-type': '/application/json', @@ -86,7 +89,7 @@ export async function deleteUserAccount(userAccount:Account) { const content = JSON.stringify({id:userAccount.id}) //Create delete request to delete the account - const response = await fetch('api/accounts',{ + const response = await fetch(requestUrl,{ method: "DELETE", headers: { "content-type": "/application/json", diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts index d339a27..52258ce 100644 --- a/app/client/src/api/Income.ts +++ b/app/client/src/api/Income.ts @@ -7,9 +7,11 @@ export interface Income { description: string } +const requestUrl = "http://localhost:3000/income" + export async function getIncome() { - const response = await fetch('/api/income', { + const response = await fetch(requestUrl, { method: "GET", headers: { "Cookie": document.cookie @@ -23,7 +25,7 @@ export async function getIncome() { } export async function postIncome(name:string, income:number, description:string): Promise{ - const response = await fetch("/api/income",{ + const response = await fetch(requestUrl,{ method: "POST", headers:{ "Cookie":document.cookie @@ -43,7 +45,7 @@ export async function postIncome(name:string, income:number, description:string) export async function putIncome(name:string, income:number, description:string) { - const response = await fetch("/api/income",{ + const response = await fetch(requestUrl,{ method: "POST", headers:{ "Cookie":document.cookie @@ -58,5 +60,29 @@ export async function putIncome(name:string, income:number, description:string) console.error(response.status) } + return response.ok +} + +export async function deleteIncome(selectedIncome:Income) { + + const content = JSON.stringify({id:selectedIncome.id}) + + //Create delete request to delete the income + const response = await fetch(requestUrl,{ + method: "DELETE", + headers: { + "content-type": "/application/json", + "Cookie":document.cookie + }, + body: content + }) + + if(response.ok){ + alert("Income " + selectedIncome.name + " has been deleted") + } else { + alert("Failed to delete income " + selectedIncome.name) + console.error(response.status) + } + return response.ok } \ No newline at end of file diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 9f76d55..1d243d1 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,5 +1,5 @@ import type { promises } from "dns"; -import { get } from "http"; +import { get, request } from "http"; import { json } from "stream/consumers"; import type { updateResponse } from "../type/responseTypes"; @@ -13,13 +13,16 @@ export interface Transaction{ recipient?: string; date: Date; } + +const requestUrl = "http://localhost:3000/transacitons" + //Sends a GET request to get the list of user transactions for the account export async function getTransactions(account_id: number): Promise{ //Put as object to convert to json when sent in the body const content = {"id": account_id} - const response = await fetch("api/transactions", { + const response = await fetch(requestUrl, { method: "GET", headers: { "Cookie": document.cookie @@ -39,7 +42,7 @@ export async function getTransactions(account_id: number): Promise{ - const response = await fetch("api/transacitons", { + const response = await fetch(requestUrl, { method: "POST", headers: { "content-type": "application/json", @@ -61,7 +64,7 @@ export async function pushTranscations(trans:Transaction[]):Promise{ - const response = await fetch("api/transacitons", { + const response = await fetch(requestUrl, { method: "PUT", headers: { "content-type": "application/json", @@ -84,7 +87,7 @@ export async function deleteTransaction(selectedTransaction:Transaction) { const content = JSON.stringify({id:selectedTransaction.id, financialAccount_id:selectedTransaction.financialAccount_id}) //Create delete request to delete the account - const response = await fetch('api/accounts',{ + const response = await fetch(requestUrl,{ method: "DELETE", headers: { "content-type": "/application/json", From d559598fc414665b3a226e60e03babbbf87b4e9c Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 27 Feb 2026 19:53:34 -0600 Subject: [PATCH 104/204] Fixed api requests --- app/client/src/api/Account.ts | 120 +++++++------ app/client/src/api/Income.ts | 140 +++++++++------ app/client/src/api/Transaction.ts | 163 ++++++++++-------- app/client/src/components/ui/AccountForm.tsx | 49 ++++-- app/client/src/components/ui/IncomeForm.tsx | 49 +++--- .../src/components/ui/TransationForm.tsx | 65 ++++--- .../services/database/src/routes/account.ts | 4 +- 7 files changed, 351 insertions(+), 239 deletions(-) diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index 8da0608..ecc9f90 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,4 +1,3 @@ -import { request } from "http"; import type { updateResponse } from "../type/responseTypes"; //Coppied from module @@ -17,70 +16,87 @@ const requestUrl = 'http://localhost:3000/accounts' //Sends a request to get different accounts the user has export async function getUserAccounts(): Promise { - //Sends a http request and waits for a response - const response = await fetch(requestUrl, { - method: 'GET', - headers: { - 'Cookie': document.cookie - } - }) + try{ + //Sends a http request and waits for a response + const response = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Cookie': document.cookie + }, + credentials: "include" + }) - //Determine if we were able to retrieve user's data - if(!response.ok) { + //Determine if we were able to retrieve user's data + if(!response.ok) { - //Failed to retrieve user data, return empty array - console.error("Error: Failed to retrieve users accounts ", response.status) + //Failed to retrieve user data, return empty array + console.error("Error: Failed to retrieve users accounts ", response.status) + } + + return response.json() + } catch(error) { + console.error(error) + throw error } - - return response.json() } //Sends a post request to create a user account export async function postUserAccount(newAccount:Account):Promise { - //Create post request and wait for response - const response = await fetch(requestUrl, { - method: "POST", - headers: { - 'content-type': '/application/json', - 'Cookie': document.cookie - }, - body: JSON.stringify(newAccount) - } ) - - //Determine if our post was a success - if(response.ok){ - alert("Account " + newAccount.name + " has been created" ) - } else { - alert("Failed to create account " + newAccount.name) - console.error(response.status) + try{ + //Create post request and wait for response + const response = await fetch(requestUrl, { + method: "POST", + headers: { + 'content-type': '/application/json' + }, + credentials:"include", + body: JSON.stringify(newAccount) + } ) + + //Determine if our post was a success + if(response.ok){ + alert("Account " + newAccount.name + " has been created" ) + } else { + alert("Failed to create account " + newAccount.name) + console.error(response.status) + } + + return response.json() + + } catch(error) { + console.error(error) + throw error } - - return response.json() } //Updates the users account export async function putUserAccount(newAccount:Account):Promise { - //Create post request and wait for response - const response = await fetch(requestUrl, { - method: "PUT", - headers: { - 'content-type': '/application/json', - 'Cookie': document.cookie - }, - body: JSON.stringify(newAccount) - } ) - - //Determine if our post was a success - if(response.ok){ - alert("Account " + newAccount.name + " has been updated" ) - } else { - alert("Failed to update account " + newAccount.name) - console.error(response.status) + try{ + //Create post request and wait for response + const response = await fetch(requestUrl, { + method: "PUT", + headers: { + 'content-type': 'application/json', + }, + credentials: "include", + body: JSON.stringify(newAccount) + } ) + + //Determine if our post was a success + if(response.ok){ + alert("Account " + newAccount.name + " has been updated" ) + } else { + alert("Failed to update account " + newAccount.name) + console.error(response.status) + } + + return response.json() + } catch (error) { + console.error(error) + throw error } - - return response.json() } @@ -92,9 +108,9 @@ export async function deleteUserAccount(userAccount:Account) { const response = await fetch(requestUrl,{ method: "DELETE", headers: { - "content-type": "/application/json", - "Cookie":document.cookie + "content-type": "/application/json" }, + credentials:"include", body: content }) diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts index 52258ce..a29613f 100644 --- a/app/client/src/api/Income.ts +++ b/app/client/src/api/Income.ts @@ -1,3 +1,4 @@ +import { resumeAndPrerenderToNodeStream } from "react-dom/static" import type { updateResponse } from "../type/responseTypes" export interface Income { @@ -9,80 +10,105 @@ export interface Income { const requestUrl = "http://localhost:3000/income" -export async function getIncome() { +export async function getIncome():Promise { - const response = await fetch(requestUrl, { - method: "GET", - headers: { - "Cookie": document.cookie + try { + const response = await fetch(requestUrl, { + method: "GET", + credentials: "include" + }) + + if(!response.ok) { + console.error(response.status) + alert("Failed to retrieve income") + return [] + } else { + return response.json() } - }) - - if(!response.ok) { - console.error(response.status) - alert("Failed to retrieve income") + } catch(error) { + console.log(error) + throw error } } export async function postIncome(name:string, income:number, description:string): Promise{ - const response = await fetch(requestUrl,{ - method: "POST", - headers:{ - "Cookie":document.cookie - }, - body: JSON.stringify({"name":name,"income":income,"description":description}) - }) - - if(response.ok){ - alert("Created income " + name) - } else { - alert("Failed to create income " + name) - console.error(response.status) - } + try{ + const response = await fetch(requestUrl,{ + method: "POST", + headers:{ + "content-type": 'application/json' + }, + credentials: "include", + body: JSON.stringify({"name":name,"income":income,"description":description}) + }) + + if(response.ok){ + alert("Created income " + name) + } else { + alert("Failed to create income " + name) + console.error(response.status) + } - return response.json() + return response.json() + } catch(error) { + console.error(error) + throw error + } } export async function putIncome(name:string, income:number, description:string) { - const response = await fetch(requestUrl,{ - method: "POST", - headers:{ - "Cookie":document.cookie - }, - body: JSON.stringify({"name":name,"income":income,"description":description}) - }) - - if(response.ok){ - alert("Created income " + name) - } else { - alert("Failed to create income " + name) - console.error(response.status) + + try { + const response = await fetch(requestUrl,{ + method: "POST", + headers:{ + "content-type": "applicaton/json" + }, + credentials: "include", + body: JSON.stringify({"name":name,"income":income,"description":description}) + }) + + if(response.ok){ + alert("Created income " + name) + } else { + alert("Failed to create income " + name) + console.error(response.status) + } + + return response.ok + } catch(error) { + console.log(error) + throw error } - return response.ok } export async function deleteIncome(selectedIncome:Income) { const content = JSON.stringify({id:selectedIncome.id}) - - //Create delete request to delete the income - const response = await fetch(requestUrl,{ - method: "DELETE", - headers: { - "content-type": "/application/json", - "Cookie":document.cookie - }, - body: content - }) - - if(response.ok){ - alert("Income " + selectedIncome.name + " has been deleted") - } else { - alert("Failed to delete income " + selectedIncome.name) - console.error(response.status) + try{ + //Create delete request to delete the income + const response = await fetch(requestUrl,{ + method: "DELETE", + headers: { + "content-type": "/application/json", + }, + credentials:"include", + body: content + }) + + if(response.ok){ + alert("Income " + selectedIncome.name + " has been deleted") + } else { + alert("Failed to delete income " + selectedIncome.name) + console.error(response.status) + } + + return response.ok + + } catch(error) { + console.error(error) + throw error } - - return response.ok } \ No newline at end of file diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 1d243d1..89d5b3b 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -19,89 +19,108 @@ const requestUrl = "http://localhost:3000/transacitons" //Sends a GET request to get the list of user transactions for the account export async function getTransactions(account_id: number): Promise{ - //Put as object to convert to json when sent in the body - const content = {"id": account_id} - - const response = await fetch(requestUrl, { - method: "GET", - headers: { - "Cookie": document.cookie - }, - body: JSON.stringify(content) - }) - - if(!response.ok){ - - alert("Failed to retrieve account's transaction\n") - console.error(response.status) + try { + //Put as object to convert to json when sent in the body + const content = {"id": account_id} + + const response = await fetch(requestUrl, { + method: "GET", + credentials: "include", + body: JSON.stringify(content) + }) + + if(!response.ok){ + + alert("Failed to retrieve account's transaction\n") + console.error(response.status) + } + + return response.json() + } catch(error){ + console.error(error) + throw error } - return response.json() } //Can send multiple transactions in a push request -export async function pushTranscations(trans:Transaction[]):Promise{ - - const response = await fetch(requestUrl, { - method: "POST", - headers: { - "content-type": "application/json", - "Cookie": document.cookie, - }, - body: JSON.stringify(trans) - }) - - if(response.ok){ - alert("Create the transaction") - } else { - alert("Failed to create transaction") - console.error(response.status) +export async function postTranscations(trans:Transaction[]):Promise{ + + try { + const response = await fetch(requestUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + credentials:"include", + body: JSON.stringify(trans) + }) + + if(response.ok){ + alert("Create the transaction") + } else { + alert("Failed to create transaction") + console.error(response.status) + } + + return response.json() + } catch(error) { + console.log(error) + throw(error) } - - return response.json() } -//Can send multiple transactions in a push request -export async function putTranscations(trans:Transaction):Promise{ - - const response = await fetch(requestUrl, { - method: "PUT", - headers: { - "content-type": "application/json", - "Cookie": document.cookie, - }, - body: JSON.stringify(trans) - }) - - if(response.ok){ - alert("Updated the transaction") - } else { - alert("Failed to update transaction") - console.error(response.status) +export async function putTranscations(trans:Transaction):Promise{ + + try { + const response = await fetch(requestUrl, { + method: "PUT", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: JSON.stringify(trans) + }) + + if(response.ok){ + alert("Updated the transaction") + } else { + alert("Failed to update transaction") + console.error(response.status) + } + + return response.ok + + } catch(error){ + console.log(error) + throw error } - - return response.json() } export async function deleteTransaction(selectedTransaction:Transaction) { - - const content = JSON.stringify({id:selectedTransaction.id, financialAccount_id:selectedTransaction.financialAccount_id}) - //Create delete request to delete the account - const response = await fetch(requestUrl,{ - method: "DELETE", - headers: { - "content-type": "/application/json", - "Cookie":document.cookie - }, - body: content - }) - - if(response.ok){ - alert("Transaction has been deleted") - } else { - alert("Failed to delete transaction") - console.error(response.status) + try { + const content = JSON.stringify({id:selectedTransaction.id, financialAccount_id:selectedTransaction.financialAccount_id}) + //Create delete request to delete the account + const response = await fetch(requestUrl,{ + method: "DELETE", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: content + }) + + if(response.ok){ + alert("Transaction has been deleted") + } else { + alert("Failed to delete transaction") + console.error(response.status) + } + + return response.ok + + } catch(error) { + console.log(error) + throw error } - - return response.ok } \ No newline at end of file diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index 0a99c1f..da7d4ad 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' -import {postUserAccount, type Account} from '../../api/Account.ts' +import {postUserAccount, putUserAccount, type Account} from '../../api/Account.ts' import { validateAccountForm } from "../../util/ValidateForms.ts"; import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput.ts"; interface popupProp{ toggle: () => void; setAccount: (account:Account) => void + addAccount: (account:Account) => void edit: boolean selectedAccount?: Account } @@ -21,7 +22,7 @@ export const accountCategory = { type typeofAccount = keyof typeof accountCategory -export default function popupForm({toggle, setAccount, edit, selectedAccount}:popupProp){ +export default function popupForm({toggle, setAccount, addAccount, edit, selectedAccount}:popupProp){ //Submits the account data @@ -45,18 +46,44 @@ export default function popupForm({toggle, setAccount, edit, selectedAccount}:po const newAccount: Account = {id: 0, name:formInput.name, type: accountType, balance: accountBalance, subtype:"", value:0, last_updated: new Date()} console.log(newAccount) - //Send a post request to create or edit the account - postUserAccount(newAccount).then((data)=>{ + //Determine if we edding an account info + if(edit && selectedAccount) { + newAccount.id = selectedAccount.id + + try{ + //Put reques to update the account + putUserAccount(newAccount).then((response) =>{ + + //Determine if sucessfully post + if(response && response.lastUpdated) { + newAccount.last_updated = response.lastUpdated + addAccount(newAccount) + } + }) + } catch(error) { + + } - //When creating new account, id is returned for that account - if(data && data["id"]) - newAccount["id"] = data["id"] + } else { + try{ + //Send a post request to create or edit the account + postUserAccount(newAccount).then((response)=>{ + + //When creating new account, id is returned for that account + if(response && response.id) + newAccount.id = response.id - - if(data["lastUpdated"]) - newAccount["last_updated"] = data["lastUpdated"] - }) + if(response.lastUpdated) + newAccount.last_updated = response.lastUpdated + + setAccount(newAccount) + }) + } catch(error) { + alert("Failed to create account " + newAccount.name) + } + } + toggle() } else { alert("Please fill out the form") diff --git a/app/client/src/components/ui/IncomeForm.tsx b/app/client/src/components/ui/IncomeForm.tsx index fda9515..6037f53 100644 --- a/app/client/src/components/ui/IncomeForm.tsx +++ b/app/client/src/components/ui/IncomeForm.tsx @@ -35,16 +35,17 @@ export default function popupForm({toggle,setIncome, addIncome, edit, selectedIn const newIncome: Income = {id: 0, name:name, income:inputAmount, description:description} //Determine if response gave back an id for the new created income - if(data) { + if(data && data.id) { + + newIncome.id = data.id + addIncome(newIncome) - if(data.id){ - - newIncome.id = data.id - addIncome(newIncome) - } } }) } + toggle() + } else { + alert("Please fillout the income form") } } @@ -53,27 +54,31 @@ export default function popupForm({toggle,setIncome, addIncome, edit, selectedIn const [description, setDescription] = useState('') return( -
-

Income

-

+ <> +
+ +
+ {edit ? (

Edit Income

):(

Create Income

)} +

- - setName(event.target.value)}/> -

+ + setName(event.target.value)}/> +

- - handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event,amount,setAmount)}/> -

+ + handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event,amount,setAmount)}/> +

- - setDescription(event.target.value)}/> -

+ + setDescription(event.target.value)}/> +

-
- - {edit? (): ()} +
+ + {edit? (): ()} +
- + ) } \ No newline at end of file diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index b4e1ae3..fced209 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -3,12 +3,13 @@ import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' import { getUserAccounts, type Account } from "../../api/Account"; import { validateTransactionForm } from "../../util/ValidateForms"; -import { pushTranscations, type Transaction } from "../../api/Transaction"; -import { handleCurrencyChange } from "../../util/handleInput"; +import { postTranscations, putTranscations, type Transaction } from "../../api/Transaction"; +import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput"; interface popupProp{ toggle: () => void; - setTransaction: () => void + setTransaction: (editedTransaction:Transaction) => void + addTransaction: (newTransaction:Transaction) => void edit: boolean selectedTransaction?: Transaction } @@ -26,7 +27,7 @@ export const transactionCategory = { type typeOfTransaction = keyof typeof transactionCategory //Returns a form of for the user to enter their info -export default function popupForm({toggle, edit, selectedTransaction}:popupProp){ +export default function popupForm({toggle,setTransaction,addTransaction, edit, selectedTransaction}:popupProp){ //Handles the submiting the form const handleSubmit = () =>{ @@ -40,17 +41,40 @@ export default function popupForm({toggle, edit, selectedTransaction}:popupProp) userTransaction = {id: 0, financialAccount_id:accountId, amount:transferAmount, type: selectedType, date: new Date()} if(edit && selectedTransaction) { - - userTransaction["id"] = selectedTransaction["id"] - } - - pushTranscations([userTransaction]).then((data) => {data[0] - - //Successful push if data is returned - if(data && data[0]["id"]) { - userTransaction["id"] = data[0]["id"] + try{ + //Editing selected transaction + userTransaction.id = selectedTransaction.id + + //Send a request to update the transaction + putTranscations(userTransaction).then((result) => { + //Determine if the + if(result) { + setTransaction(userTransaction) + } + }) + } catch(error){ + alert("Failed to edit transaction") + } + } else { + + try{ + //Send a request to create the transaction + postTranscations([userTransaction]).then((response) => { + + //Successful put if response is returned + if(response && response[0].id) { + userTransaction.id = response[0].id + addTransaction(userTransaction) + } else { + alert("Failed to create transaction") + } + + + }) + } catch(error) { + alert("Failed to create transaction") } - }) + } //Need to insert function that sets the state in parent component @@ -61,13 +85,7 @@ export default function popupForm({toggle, edit, selectedTransaction}:popupProp) } - const handleCurrencyBlur = (event:React.ChangeEvent) => { - if(event.target.value !== "") { - setAmount(parseFloat(amount).toFixed(2)) - } - - } - + //Handle when the user adds a file to the form const handleFile = (event:React.ChangeEvent) =>{ if(event.target && event.target.files && event.target.files[0]){ @@ -76,8 +94,7 @@ export default function popupForm({toggle, edit, selectedTransaction}:popupProp) setFile(undefined) } - //Insert cvs parsing function - + //Insert cvs parsing function/validation function } @@ -132,7 +149,7 @@ export default function popupForm({toggle, edit, selectedTransaction}:popupProp)

- handleCurrencyChange(event, setAmount)} onBlur={handleCurrencyBlur} placeholder="0.00"/> + handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} placeholder="0.00"/>

{edit ? (null):( diff --git a/app/server/services/database/src/routes/account.ts b/app/server/services/database/src/routes/account.ts index 0f0aaa5..b50bc78 100644 --- a/app/server/services/database/src/routes/account.ts +++ b/app/server/services/database/src/routes/account.ts @@ -20,8 +20,10 @@ accountsRouter.post("/", async (req: Request, res: Response) => { res.json({ message: "Account successfully created", - id: result.insertId + id: result.insertId, + }); + res.append("Access-Control-Allow-Origin", "http://localhost:8080/") } catch (err) { console.error("Account creation failed", err); res.status(500).json({ error: "Account creation failed" }); From 54e02b377dd7ba3469f46e23c39fb181dddb6119 Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 27 Feb 2026 20:26:10 -0600 Subject: [PATCH 105/204] auth tests + prettier --- app/server/docker-compose.yml | 20 --- .../services/ts/api-gateway/src/index.ts | 115 ++++++++------- app/server/services/ts/auth/bun.lockb | Bin 11118 -> 6418 bytes app/server/services/ts/auth/package.json | 3 +- app/server/services/ts/auth/src/config.ts | 4 + app/server/services/ts/auth/src/index.ts | 22 +-- app/server/services/ts/auth/src/logic.ts | 103 +++---------- app/server/services/ts/auth/src/parsing.ts | 14 +- app/server/services/ts/auth/src/queries.ts | 9 +- app/server/services/ts/auth/src/secrets.ts | 28 ++++ app/server/services/ts/auth/src/validation.ts | 22 +++ .../services/ts/auth/test/logic.test.ts | 99 +++++++++++++ .../services/ts/auth/test/parsing.test.ts | 57 ++++++++ .../services/ts/auth/test/validation.test.ts | 48 +++++++ app/server/services/ts/bun.lockb | Bin 72600 -> 110427 bytes .../common/{corsUtil.ts => expressUtils.ts} | 19 ++- app/server/services/ts/common/sqlUtil.ts | 12 ++ app/server/services/ts/common/types.ts | 28 ++-- app/server/services/ts/package.json | 8 +- app/server/services/ts/user/src/index.ts | 135 ++++++++++++------ app/server/services/ts/vitest.config.ts | 10 ++ 21 files changed, 525 insertions(+), 231 deletions(-) create mode 100644 app/server/services/ts/auth/src/config.ts create mode 100644 app/server/services/ts/auth/src/secrets.ts create mode 100644 app/server/services/ts/auth/src/validation.ts create mode 100644 app/server/services/ts/auth/test/logic.test.ts create mode 100644 app/server/services/ts/auth/test/parsing.test.ts create mode 100644 app/server/services/ts/auth/test/validation.test.ts rename app/server/services/ts/common/{corsUtil.ts => expressUtils.ts} (51%) create mode 100644 app/server/services/ts/common/sqlUtil.ts create mode 100644 app/server/services/ts/vitest.config.ts diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index cb837dd..d01263b 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -3,10 +3,6 @@ services: build: context: services/ts dockerfile: api-gateway/Dockerfile - cache_from: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-api-gateway:build - cache_to: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-api-gateway:build,mode=max image: $DOCKER_HUB_USERNAME/finus-api-gateway:$RELEASE_VERSION env_file: - path: ./default_container.env @@ -24,10 +20,6 @@ services: build: context: services/ts dockerfile: auth/Dockerfile - cache_from: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-auth:build - cache_to: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-auth:build,mode=max image: $DOCKER_HUB_USERNAME/finus-auth:$RELEASE_VERSION env_file: - path: ./default_container.env @@ -43,10 +35,6 @@ services: build: context: services/ts dockerfile: user/Dockerfile - cache_from: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-user:build - cache_to: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-user:build,mode=max image: $DOCKER_HUB_USERNAME/finus-user:$RELEASE_VERSION env_file: - path: ./default_container.env @@ -60,10 +48,6 @@ services: build: context: services/python/analytics dockerfile: Dockerfile - cache_from: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-analytics:build - cache_to: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-analytics:build,mode=max image: $DOCKER_HUB_USERNAME/finus-analytics:$RELEASE_VERSION env_file: - path: ./default_container.env @@ -77,10 +61,6 @@ services: build: context: services/python/market dockerfile: Dockerfile - cache_from: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-market:build - cache_to: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-market:build,mode=max image: $DOCKER_HUB_USERNAME/finus-market:$RELEASE_VERSION env_file: - path: ./default_container.env diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 2f4fc3e..46cc27f 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -1,8 +1,8 @@ import { PORT } from "@/port"; -import { buildCorsConfig } from "@/corsUtil"; +import { buildCorsConfig } from "@/expressUtils"; import { onExit } from "@/hooks"; import express from "express"; -import { createProxyMiddleware } from 'http-proxy-middleware'; +import { createProxyMiddleware } from "http-proxy-middleware"; const app = express(); @@ -11,8 +11,8 @@ app.use( pathFilter: "/api/signup", target: process.env.AUTH_SERVICE_ADDR, changeOrigin: true, - pathRewrite: { "^/api/signup": "/signup" } - }) + pathRewrite: { "^/api/signup": "/signup" }, + }), ); app.use( @@ -23,7 +23,7 @@ app.use( pathRewrite: { "^/api/login": "/login", }, - }) + }), ); app.get("/health", async (_req: express.Request, res: express.Response) => { @@ -43,7 +43,6 @@ app.get("/health", async (_req: express.Request, res: express.Response) => { res.json(result); }); - app.use(buildCorsConfig(undefined)); app.use(express.json()); @@ -52,60 +51,80 @@ const server = app.listen(PORT, () => { }); onExit(async () => await server.close()); - //API gateway sits on port 3000 and is accessible from there. Go to browser and type http://localhost:3000/health and you should see which services are up. -app.get('/health', async (req: express.Request, res: express.Response) => { - const result: { [key: string]: string} = {}; - const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); +app.get("/health", async (req: express.Request, res: express.Response) => { + const result: { [key: string]: string } = {}; + const services: string[] = Object.keys(process.env).filter((x) => + /^.*_SERVICE_ADDR$/.test(x), + ); - for(const service of services) { - const serviceName = service.split('_')[0]; - result[serviceName] = await fetch(`${process.env[service]}/health`) - .then((res) => res.text()) - .catch((err) => err.message); - console.log("received response"); - } - - res.json(result); + for (const service of services) { + const serviceName = service.split("_")[0]; + result[serviceName] = await fetch(`${process.env[service]}/health`) + .then((res) => res.text()) + .catch((err) => err.message); + console.log("received response"); + } + + res.json(result); }); -app.get('/charts/expenses', async (req: express.Request, res: express.Response) => { - console.log("Received request for expenses chart data with query:", req.query); - const period = req.query.period as string; - const response = await fetch(`${process.env.USER_SERVICE_ADDR}/charts/expenses?period=${period}`) +app.get( + "/charts/expenses", + async (req: express.Request, res: express.Response) => { + console.log( + "Received request for expenses chart data with query:", + req.query, + ); + const period = req.query.period as string; + const response = await fetch( + `${process.env.USER_SERVICE_ADDR}/charts/expenses?period=${period}`, + ) .then((res) => res.json()) .catch((err) => { - console.error("Error fetching expenses chart data:", err); - res.status(500).json({ error: "Failed to fetch expenses chart data" }); + console.error("Error fetching expenses chart data:", err); + res.status(500).json({ error: "Failed to fetch expenses chart data" }); }); - res.json(response); -}); - - + res.json(response); + }, +); -app.get('/charts/savings', async (req: express.Request, res: express.Response) => { - console.log("Received request for savings chart data with query:", req.query); - const period = req.query.period as string; - const response = await fetch(`${process.env.ANALYTICS_SERVICE_ADDR}/charts/savings?period=${period}`) +app.get( + "/charts/savings", + async (req: express.Request, res: express.Response) => { + console.log( + "Received request for savings chart data with query:", + req.query, + ); + const period = req.query.period as string; + const response = await fetch( + `${process.env.ANALYTICS_SERVICE_ADDR}/charts/savings?period=${period}`, + ) .then((res) => res.json()) .catch((err) => { - console.error("Error fetching savings chart data:", err); - res.status(500).json({ error: "Failed to fetch savings chart data" }); + console.error("Error fetching savings chart data:", err); + res.status(500).json({ error: "Failed to fetch savings chart data" }); }); - res.json(response); -}); - - - + res.json(response); + }, +); -app.get('/charts/incomeflow', async (req: express.Request, res: express.Response) => { - console.log("Received request for income chart data with query:", req.query); - const period = req.query.period as string; - const response = await fetch(`${process.env.ANALYTICS_SERVICE_ADDR}/charts/incomeflow?period=${period}`) +app.get( + "/charts/incomeflow", + async (req: express.Request, res: express.Response) => { + console.log( + "Received request for income chart data with query:", + req.query, + ); + const period = req.query.period as string; + const response = await fetch( + `${process.env.ANALYTICS_SERVICE_ADDR}/charts/incomeflow?period=${period}`, + ) .then((res) => res.json()) .catch((err) => { - console.error("Error fetching income chart data:", err); - res.status(500).json({ error: "Failed to fetch income chart data" }); + console.error("Error fetching income chart data:", err); + res.status(500).json({ error: "Failed to fetch income chart data" }); }); - res.json(response); -}); + res.json(response); + }, +); diff --git a/app/server/services/ts/auth/bun.lockb b/app/server/services/ts/auth/bun.lockb index afae96f7625dd6a62b919d60ab4d5e4c8984e518..c9d0fb033da37250057cce6ddf83c94391808f4e 100755 GIT binary patch delta 1109 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 A7ytkO diff --git a/app/server/services/ts/auth/package.json b/app/server/services/ts/auth/package.json index f2a9565..7e48891 100644 --- a/app/server/services/ts/auth/package.json +++ b/app/server/services/ts/auth/package.json @@ -2,8 +2,7 @@ "name": "auth", "main": "src/index.ts", "dependencies": { - "jsonwebtoken": "^9.0.3", - "mysql2": "^3.15.3" + "jsonwebtoken": "^9.0.3" }, "scripts": { "dev": "bun --watch src/index.ts", diff --git a/app/server/services/ts/auth/src/config.ts b/app/server/services/ts/auth/src/config.ts new file mode 100644 index 0000000..4918864 --- /dev/null +++ b/app/server/services/ts/auth/src/config.ts @@ -0,0 +1,4 @@ +export const JWT_SECRET = process.env.JWT_SECRET; +export const MIN_PASSWORD_LENGTH = 8; +export const MIN_AGE = 1; +export const MAX_AGE = 120; diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts index d2fbe3f..e3ab10b 100644 --- a/app/server/services/ts/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -1,23 +1,13 @@ -import mysql from "mysql2/promise"; import express from "express"; import { PORT } from "@/port"; import { onExit } from "@/hooks"; -import { buildCorsConfig } from "@/corsUtil"; +import { buildCorsConfig, handleServerError } from "@/expressUtils"; import { signup, login } from "./logic"; import type { LoginBody, SignupBody } from "./types"; import { parseLoginBody, parseSignupBody } from "./parsing"; +import { getConnectionPool } from "@/sqlUtil"; -const pool = mysql.createPool({ - host: process.env.MYSQL_HOST, - port: Number(process.env.MYSQL_PORT), - user: 'root',//process.env.MYSQL_USER, // ------------------------------------ Needs fixing, finus_app gets denied access, this might be an issue of accessing the db from outside its container - password: process.env.MYSQL_PASSWORD, - database: process.env.DB_NAME, - waitForConnections: true, - connectionLimit: 10, -}); - - +const pool = getConnectionPool(); const app = express(); app.use(express.json()); app.use(buildCorsConfig(undefined)); @@ -32,7 +22,7 @@ app.post("/signup", async (req, res) => { } console.log("In Index, calling for signup function"); - signup(body, res, pool); + handleServerError(() => signup(body, res, pool), res); }); app.post("/login", async (req, res) => { @@ -44,11 +34,11 @@ app.post("/login", async (req, res) => { return; } - login(body, res, pool); + handleServerError(() => login(body, res, pool), res); }); app.get("/health", (_, res) => { - res.send({ok: true}); + res.send({ ok: true }); }); const server = app.listen(PORT, () => { diff --git a/app/server/services/ts/auth/src/logic.ts b/app/server/services/ts/auth/src/logic.ts index fe134d2..1d67e94 100644 --- a/app/server/services/ts/auth/src/logic.ts +++ b/app/server/services/ts/auth/src/logic.ts @@ -1,109 +1,52 @@ -import { type LoginBody, type SignupBody, type AuthTokenClaims } from "./types"; -import { insertUser, getUserWithPasswordByEmail } from "./queries"; -// import { emailExists } from "./queries"; +import { type LoginBody, type SignupBody } from "./types"; +import { + createUser, + getUserByEmail, + accountWithEmailExists, +} from "./queries"; +import { validateSignupBody } from "./validation"; +import { passwordMatchesHash, generateJWT, hashPassword } from "./secrets.ts"; import { type User } from "@/types"; -import jwt from "jsonwebtoken"; - -const JWT_SECRET = process.env.JWT_SECRET; -const MIN_PASSWORD_LENGTH = 8; -const MIN_AGE = 1; -const MAX_AGE = 120; export async function signup(body: SignupBody, res, pool) { - let invalidReason: string | undefined = undefined; - - if (!validateEmail(body.email)) { - invalidReason = "Please enter a valid email address."; - } else if (!validateAge(body.age)) { - invalidReason = `Age must be a whole number between ${MIN_AGE} and ${MAX_AGE}.`; - } else if (!validatePassword(body.password)) { - invalidReason = - "Password must be at least 8 characters and include letters and numbers."; - } - - if (invalidReason) { + try { + validateSignupBody(body); + } catch (e) { res.status(400).json({ - error: invalidReason, + error: e.message, }); return; } - console.log("Passed email, pwd and age checks, going to check for existing email in db"); - - try { - //const existingUser = await emailExists(body.email, pool); - const existingUser = false;//debugging email check for now - if (existingUser) { - res.status(409).json({ - error: "An account with this email already exists.", - }); - return; - } - } catch { - res.status(500).json({ error: "Could not validate account state." }); - return; - } - - const passwordHash = await Bun.password.hash(body.password, { - algorithm: "bcrypt", - }); - try { - console.log("Inserting user into database..."); - await insertUser({ ...body, pw_hash: passwordHash }, pool); - } catch (error) { - res.status(500).json({ error: error.message }); + const existing = await accountWithEmailExists(body.email, pool); + if (existing) { + res.status(409).json({ + error: "An account with this email already exists.", + }); return; } - res.status(201).json({}); -} - -function validateEmail(email: string): boolean { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); -} - -function validateAge(age: number): boolean { - return age > MIN_AGE && age < MAX_AGE; -} + const hash = await hashPassword(body.password); + await createUser({ ...body, pw_hash: hash }, pool); -function validatePassword(password: string): boolean { - const hasLetter = /[a-zA-Z]/.test(password); - const hasDigit = /\d/.test(password); - return password.length >= MIN_PASSWORD_LENGTH && hasLetter && hasDigit; + res.status(201); } export async function login(body: LoginBody, res, pool): void { let user: User; try { - user = await getUserWithPasswordByEmail(body.email, pool); + user = await getUserByEmail(body.email, pool); } catch { res.status(401).json({ error: "Invalid email or password." }); return; } - if (!user || !verifyPassword(user, body.password)) { + if (!passwordMatchesHash(body.password, user.pw_hash)) { res.status(401).json({ error: "Invalid email or password." }); return; } - const token = generateToken(user); res.status(200).json({ - token, + token: generateJWT(user), }); } - -function verifyPassword(user: User, password: string): boolean { - return Bun.password.verifySync(password, user.pw_hash); -} - -function generateToken(user: User): string { - const payload: AuthTokenClaims = { - sub: String(user.id), - email: user.email, - name: user.name, - first_name: user.first_name, - last_name: user.last_name, - }; - - return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" }); -} diff --git a/app/server/services/ts/auth/src/parsing.ts b/app/server/services/ts/auth/src/parsing.ts index cb25944..9820790 100644 --- a/app/server/services/ts/auth/src/parsing.ts +++ b/app/server/services/ts/auth/src/parsing.ts @@ -1,10 +1,20 @@ import type { LoginBody, SignupBody } from "./types"; import { validateType } from "@/types"; +function normalizeEmail(email: string): boolean { + const normalized = email?.trim().toLowerCase(), + isValidFormat = /^\w+@\w+\.\w+$/.test(normalized); + if (!isValidFormat) { + throw new Error("invalid email format"); + } + return normalized; +} + export function parseLoginBody(obj: unknown): LoginBody { validateType(obj, ["email", "password"]); + return { - email: obj.email?.trim().toLowerCase(), + email: normalizeEmail(obj.email), password: obj.password, }; } @@ -30,7 +40,7 @@ export function parseSignupBody(obj: unknown): SignupBody { username: obj.username.trim(), password: obj.password, created: obj.created, - email: obj.email.trim(), + email: normalizeEmail(obj.email), age: idInt, }; } diff --git a/app/server/services/ts/auth/src/queries.ts b/app/server/services/ts/auth/src/queries.ts index 28905b2..eea094c 100644 --- a/app/server/services/ts/auth/src/queries.ts +++ b/app/server/services/ts/auth/src/queries.ts @@ -1,5 +1,5 @@ import type { UserWithPassword } from "./types"; -export async function getUserWithPasswordByEmail( +export async function getUserByEmail ( email: string, pool, ): Promise { @@ -29,7 +29,10 @@ export async function getUserWithPasswordByEmail( return rows[0]; } -export async function emailExists(email: string, pool): Promise { +export async function accountWithEmailExists( + email: string, + pool, +): Promise { return await pool .execute(`SELECT COUNT(*) as count FROM finusAccount WHERE email = ?`, [ email, @@ -39,7 +42,7 @@ export async function emailExists(email: string, pool): Promise { }); } -export async function insertUser(user: UserWithPassword, pool): void { +export async function createUser(user: UserWithPassword, pool): void { const connection = await pool.getConnection(); try { await connection.beginTransaction(); diff --git a/app/server/services/ts/auth/src/secrets.ts b/app/server/services/ts/auth/src/secrets.ts new file mode 100644 index 0000000..a98d426 --- /dev/null +++ b/app/server/services/ts/auth/src/secrets.ts @@ -0,0 +1,28 @@ +import type { AuthTokenClaims, User } from "./types"; +import { JWT_SECRET } from "./config"; +import jwt from "jsonwebtoken"; + +export function passwordMatchesHash( + password: string, + pw_hash: string, +): boolean { + return Bun.password.verifySync(password, pw_hash); +} + +export async function hashPassword(password: string) { + return await Bun.password.hash(password, { + algorithm: "bcrypt", + }); +} + +export function generateJWT(user: User): string { + const payload: AuthTokenClaims = { + sub: String(user.id), + email: user.email, + name: user.name, + first_name: user.first_name, + last_name: user.last_name, + }; + + return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" }); +} diff --git a/app/server/services/ts/auth/src/validation.ts b/app/server/services/ts/auth/src/validation.ts new file mode 100644 index 0000000..bf25979 --- /dev/null +++ b/app/server/services/ts/auth/src/validation.ts @@ -0,0 +1,22 @@ +import type { LoginBody } from "../src/types"; +import { MIN_AGE, MAX_AGE, MIN_PASSWORD_LENGTH } from "../src/config"; + +export function validateSignupBody(body: LoginBody) { + if (body.age < MIN_AGE || body.age > MAX_AGE) { + throw new Error( + `Age must be a whole number between ${MIN_AGE} and ${MAX_AGE}`, + ); + } + + if (!validatePassword(body.password)) { + throw new Error( + `Password must be at least ${MIN_PASSWORD_LENGTH} characters and include letters and numbers.`, + ); + } +} + +function validatePassword(password: string): boolean { + const hasLetter = /[a-zA-Z]/.test(password); + const hasDigit = /\d/.test(password); + return password.length >= MIN_PASSWORD_LENGTH && hasLetter && hasDigit; +} diff --git a/app/server/services/ts/auth/test/logic.test.ts b/app/server/services/ts/auth/test/logic.test.ts new file mode 100644 index 0000000..5d4494a --- /dev/null +++ b/app/server/services/ts/auth/test/logic.test.ts @@ -0,0 +1,99 @@ +vi.mock('../src/secrets.ts'); +vi.mock('../src/validation.ts'); +vi.mock('../src/queries.ts'); + +import { vi, describe, beforeEach, afterEach, expect, it } from 'vitest'; +import { login, signup } from '../src/logic'; +import type { SignupBody } from '../src/types'; + +//mocked functions +import { validateSignupBody } from '../src/validation'; +import { accountWithEmailExists, createUser, getUserByEmail } from '../src/queries'; +import { hashPassword, generateJWT, passwordMatchesHash } from '../src/secrets.ts'; + +const resJson = vi.fn(); +let res; + +beforeEach(() => { + res = { + status: vi.fn(() => ({ json: resJson})) + } +}); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("Signup behaviour", () => { + const signupBody: SignupBody = vi.fn({ + email: 'dummy email', + password: 'dummy password', + }) + + it("Should return 201 on happy path", async () => { + await signup(signupBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(201); + }); + + it("Should insert use into DB", async () => { + vi.mocked(hashPassword).mockImplementation(() => "pwhash"); + await signup(signupBody, res, undefined); + + expect(createUser).toHaveBeenCalledOnce(); + const params = vi.mocked(createUser).mock.calls[0]; + expect(params[0]?.pw_hash).toEqual('pwhash'); + }); + + it("Should 400 on invalid sign in body", async () => { + vi.mocked(validateSignupBody).mockImplementation( + () => { throw new Error() } + ); + await signup(signupBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("Should 409 on user already existing", async () => { + vi.mocked(accountWithEmailExists).mockImplementation( + () => true + ); + await signup(signupBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(409); + }); +}); + +describe("Login behaviour", () => { + const MOCK_JWT = 'JWT'; + const MOCK_USER = { pw_hash: "dummy hash"} + beforeEach(() => { + vi.mocked(generateJWT).mockImplementation(() => MOCK_JWT); + vi.mocked(passwordMatchesHash).mockImplementation(() => true); + vi.mocked(getUserByEmail).mockImplementation(() => MOCK_USER) + }); + const loginBody = { + email: 'email', + password: 'password' + }; + + it("Should return JWT on Happy path", async () => { + await login(loginBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(200); + expect(resJson).toHaveBeenCalledOnce(); + const params = vi.mocked(resJson).mock.calls[0]; + expect(params[0]?.token).toEqual(MOCK_JWT); + }); + + + it("Should 401 on no account w/ matching email", async () => { + vi.mocked(getUserByEmail).mockImplementation(() => { throw new Error() }); + await login(loginBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(401); + }); + + + it("Should 401 on wrong password", async () => { + vi.mocked(passwordMatchesHash).mockImplementation(() => false); + await login(loginBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(401); + }); +}); + diff --git a/app/server/services/ts/auth/test/parsing.test.ts b/app/server/services/ts/auth/test/parsing.test.ts new file mode 100644 index 0000000..3605f62 --- /dev/null +++ b/app/server/services/ts/auth/test/parsing.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, beforeEach } from 'vitest'; +import { parseLoginBody, parseSignupBody } from '../src/parsing.ts'; + +let signupBody; +let loginBody; +beforeEach(() => { + signupBody = { + username: 'username', + email: 'email', + first_name: 'first_name', + last_name: 'last_name', + age: '21', + password: 'password', + }; + + loginBody = { + email: 'email', + password: 'password', + }; +}); + +describe("loginBody tests", () => { + it("Should throw on non-numeric ages", () => { + loginBody.age = "twenty one"; + expect(() => parseSignupBody(signupBody)).toThrowError(); + }); + + it("Should only accept integer ages", () => { + loginBody.age = 21.25; + expect(() => parseSignupBody(signupBody)).toThrowError(); + }); + + it("Should throw on missing field", () => { + delete loginBody.username; + expect(() => parseSignupBody(signupBody)).toThrowError(); + }); +}); + + +describe("loginBody tests", () => { + it("Should lowercase email", () => { + loginBody.email = 'EMAIL@MAIL.COM'; + const parsed = parseLoginBody(loginBody); + + expect(parsed.email).toEqual(loginBody.email.toLowerCase()); + }); + + it("Should trim email", () => { + loginBody.email = ' email@mail.com '; + const parsed = parseLoginBody(loginBody); + + expect(parsed.email).toEqual(loginBody.email.trim()); + }); + +}); + + diff --git a/app/server/services/ts/auth/test/validation.test.ts b/app/server/services/ts/auth/test/validation.test.ts new file mode 100644 index 0000000..2b96c00 --- /dev/null +++ b/app/server/services/ts/auth/test/validation.test.ts @@ -0,0 +1,48 @@ +import { vi, describe, expect, it, beforeEach } from "vitest"; +import { validateSignupBody } from "../src/validation"; +import type { SignupBody } from "../src/types"; + +vi.mock('../src/config.ts', () => ({ + MIN_PASSWORD_LENGTH: 8, + MIN_AGE: 12, + MAX_AGE: 100 +})); + +let signupBody: SignupBody; +beforeEach(() => { + signupBody = { + email: "testemail@domain.com", + password: "PASSWORPASSWORDDPASSWORD123!@#", + }; +}); + +describe("Age validation tests", () => { + it("should reject ages below the minimum", () => { + signupBody.age = 11; + expect(() => validateSignupBody(signupBody)).toThrowError(); + }); + + it("should reject above below the maximum", () => { + signupBody.age = 101; + expect(() => validateSignupBody(signupBody)).toThrowError(); + }); +}); + + +describe("Password validation tests", () => { + it("should reject passwords that are too short", () => { + signupBody.password = 'PA12'; + expect(() => validateSignupBody(signupBody)).toThrowError(); + }); + + it("should reject passwords that have no letters", () => { + signupBody.password = '11111111111'; + expect(() => validateSignupBody(signupBody)).toThrowError(); + }); + + it("should reject passwords that have no digits", () => { + signupBody.password = 'aaaaaaaaaaa'; + expect(() => validateSignupBody(signupBody)).toThrowError(); + }); + +}); diff --git a/app/server/services/ts/bun.lockb b/app/server/services/ts/bun.lockb index 9d4510cfd12be4259a2129ef77f4d06e3777ebcd..f94c94bebe7541c590b5176a94961ba42e43e4c7 100755 GIT binary patch delta 37263 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 diff --git a/app/server/services/ts/common/corsUtil.ts b/app/server/services/ts/common/expressUtils.ts similarity index 51% rename from app/server/services/ts/common/corsUtil.ts rename to app/server/services/ts/common/expressUtils.ts index 677f0d8..34957fe 100644 --- a/app/server/services/ts/common/corsUtil.ts +++ b/app/server/services/ts/common/expressUtils.ts @@ -2,9 +2,10 @@ import cors from "cors"; const defaultMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]; const defaultOrigin = "http://localhost"; -export function buildCorsConfig( - opts?: { origin?: string; methods?: string[] }, -) { +export function buildCorsConfig(opts?: { + origin?: string; + methods?: string[]; +}) { return cors({ origin: opts?.origin ?? defaultOrigin, methods: opts?.methods ?? defaultMethods, @@ -12,3 +13,15 @@ export function buildCorsConfig( allowedHeaders: ["Content-Type", "Authorization"], }); } + +export async function handleServerError( + func: () => void, + res, + message: string = "Error processing request", +) { + await func().catch((e) => + res.status(500).json({ + message: message ?? e.message, + }), + ); +} diff --git a/app/server/services/ts/common/sqlUtil.ts b/app/server/services/ts/common/sqlUtil.ts new file mode 100644 index 0000000..dcd7d0d --- /dev/null +++ b/app/server/services/ts/common/sqlUtil.ts @@ -0,0 +1,12 @@ +import mysql from "mysql2/promise"; + +export const getConnectionPool = () => + mysql.createPool({ + host: process.env.MYSQL_HOST, + port: Number(process.env.MYSQL_PORT), + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.DB_NAME, + waitForConnections: true, + connectionLimit: 10, + }); diff --git a/app/server/services/ts/common/types.ts b/app/server/services/ts/common/types.ts index 0bc5603..d47c54a 100644 --- a/app/server/services/ts/common/types.ts +++ b/app/server/services/ts/common/types.ts @@ -1,22 +1,22 @@ export type User = { - id: number, - username: string, - email: string, - firstName: string, - lastName: string, - age: number, - created: string, - pw_hash: string, - salt: string, -} + id: number; + username: string; + email: string; + firstName: string; + lastName: string; + age: number; + created: string; + pw_hash: string; + salt: string; +}; export function validateType(obj: unknown, requiredKeys: string[]): void { - if(!obj) { - throw new Error('null object'); + if (!obj) { + throw new Error("null object"); } - for(const key of requiredKeys) { - if(!obj[key]) { + for (const key of requiredKeys) { + if (!obj[key]) { throw new Error(`Missing ${key}`); } } diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json index 57c164e..1ab1f64 100644 --- a/app/server/services/ts/package.json +++ b/app/server/services/ts/package.json @@ -1,6 +1,6 @@ { "scripts": { - "format": "bun prettier --write \"**/src/*.ts|**/*.ts\"", + "format": "prettier --write \"**/src/*.ts|**/*.ts\"", "prepare": "cd ../../../../ && husky hooks/.husky" }, "devDependencies": { @@ -13,13 +13,15 @@ "jiti": "^2.6.1", "minimatch": "5", "prettier": "3.8.1", - "typescript-eslint": "^8.55.0" + "typescript-eslint": "^8.55.0", + "vitest": "^4.0.18" }, "peerDependencies": { "typescript": "^5.0.0" }, "dependencies": { "cors": "^2.8.6", - "express": "^5.2.1" + "express": "^5.2.1", + "mysql2": "^3.18.1" } } diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index acfbbd3..c2af94e 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -1,6 +1,6 @@ import { PORT } from "@/port"; import { onExit } from "@/hooks"; -import { buildCorsConfig } from "@/corsUtil"; +import { buildCorsConfig } from "@/expressUtils"; import express from "express"; const app = express(); @@ -17,43 +17,98 @@ app.get("/health", (req: express.Request, res: express.Response) => { res.send("ok"); }); -app.get('/charts/expenses', (req: express.Request, res: express.Response) => { - const period = req.query.period as string; - if (!["w", "m", "y"].includes(period)) { - res.status(400).json({ error: "Invalid period. Must be 'w', 'm', or 'y'." }); - return; - } - switch(period) { - case 'w': - return res.json({ - labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - datasets: [{ - label: 'Weekly Expenses', - data: [125, 89, 210, 45, 167, 92, 78] - }] - }); - - case 'm': - return res.json({ - labels: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan', '11 Jan', '12 Jan', - '13 Jan', '14 Jan', '15 Jan', '16 Jan', '17 Jan', '18 Jan', '19 Jan', '20 Jan', '21 Jan', '22 Jan', '23 Jan', '24 Jan', - '25 Jan', '26 Jan', '27 Jan', '28 Jan', '29 Jan', '30 Jan', '31 Jan' - ], - datasets: [{ - label: 'Monthly Expenses', - data: [125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, 65, 190, 120, - 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, 143, 87 - ] - }] - }); - - case 'y': - return res.json({ - labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - datasets: [{ - label: 'Yearly Expenses', - data: [3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, 4123] - }] - }); - } +app.get("/charts/expenses", (req: express.Request, res: express.Response) => { + const period = req.query.period as string; + if (!["w", "m", "y"].includes(period)) { + res + .status(400) + .json({ error: "Invalid period. Must be 'w', 'm', or 'y'." }); + return; + } + switch (period) { + case "w": + return res.json({ + labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + datasets: [ + { + label: "Weekly Expenses", + data: [125, 89, 210, 45, 167, 92, 78], + }, + ], + }); + + case "m": + return res.json({ + labels: [ + "1 Jan", + "2 Jan", + "3 Jan", + "4 Jan", + "5 Jan", + "6 Jan", + "7 Jan", + "8 Jan", + "9 Jan", + "10 Jan", + "11 Jan", + "12 Jan", + "13 Jan", + "14 Jan", + "15 Jan", + "16 Jan", + "17 Jan", + "18 Jan", + "19 Jan", + "20 Jan", + "21 Jan", + "22 Jan", + "23 Jan", + "24 Jan", + "25 Jan", + "26 Jan", + "27 Jan", + "28 Jan", + "29 Jan", + "30 Jan", + "31 Jan", + ], + datasets: [ + { + label: "Monthly Expenses", + data: [ + 125, 89, 210, 45, 167, 92, 78, 123, 98, 134, 56, 189, 76, 143, 87, + 65, 190, 120, 134, 98, 76, 143, 87, 65, 190, 120, 134, 98, 76, + 143, 87, + ], + }, + ], + }); + + case "y": + return res.json({ + labels: [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ], + datasets: [ + { + label: "Yearly Expenses", + data: [ + 3245, 2987, 3456, 3789, 4123, 3876, 4234, 3987, 3678, 4012, 3789, + 4123, + ], + }, + ], + }); + } }); diff --git a/app/server/services/ts/vitest.config.ts b/app/server/services/ts/vitest.config.ts new file mode 100644 index 0000000..c55be0d --- /dev/null +++ b/app/server/services/ts/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' +import path from "path" + +export default defineConfig({ + resolve : { + alias: { + "@": path.resolve(__dirname, "./common") + } + } +}) From ac2cc2cd30f8d0f92765eacbe5c39f25a84636cc Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 27 Feb 2026 20:26:31 -0600 Subject: [PATCH 106/204] dockerfile changes --- app/server/services/python/analytics/Dockerfile | 4 +++- app/server/services/python/market/Dockerfile | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/server/services/python/analytics/Dockerfile b/app/server/services/python/analytics/Dockerfile index e62b461..4f1f204 100644 --- a/app/server/services/python/analytics/Dockerfile +++ b/app/server/services/python/analytics/Dockerfile @@ -5,7 +5,9 @@ RUN --mount=type=cache,target=/root/.cache WORKDIR /app COPY requirements.txt . -RUN pip install -r requirements.txt + +RUN --mount=type=cache,target=/root/.cache/pip \ +pip install -r requirements.txt COPY . . CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"] diff --git a/app/server/services/python/market/Dockerfile b/app/server/services/python/market/Dockerfile index e62b461..cb78353 100644 --- a/app/server/services/python/market/Dockerfile +++ b/app/server/services/python/market/Dockerfile @@ -1,11 +1,11 @@ +# syntax=docker/dockerfile:1 FROM python:3.11-slim -#cache pip packages -RUN --mount=type=cache,target=/root/.cache - WORKDIR /app COPY requirements.txt . -RUN pip install -r requirements.txt + +RUN --mount=type=cache,target=/root/.cache/pip \ +pip install -r requirements.txt COPY . . CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"] From 4b71469996603fac7eff3114a0f5b60c29c5e8f5 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Sat, 28 Feb 2026 00:36:12 -0600 Subject: [PATCH 107/204] Created components to create a list of accounts cards --- app/client/src/App.tsx | 3 +- app/client/src/components/ui/AccountCard.tsx | 59 +++++++++++++++++++ app/client/src/components/ui/AccountForm.tsx | 30 +++++++--- app/client/src/components/ui/AccountList.tsx | 54 +++++++++++++++++ app/client/src/components/ui/IncomeForm.tsx | 26 +++++--- .../src/components/ui/TransationForm.tsx | 8 +-- 6 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 app/client/src/components/ui/AccountCard.tsx create mode 100644 app/client/src/components/ui/AccountList.tsx diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 0cbb5f1..e1064b5 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -8,6 +8,7 @@ import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-route import LoadingSpinner from './components/ui/LoadingSpinner.tsx' import AccountPopup from './components/ui/AccountForm.tsx' import TransactionPopup from './components/ui/TransationForm.tsx' +import IncomePopup from './components/ui/IncomeForm.tsx' import type { Account } from './api/Account.ts' function App() { @@ -75,7 +76,7 @@ function NotFoundPage() {

- {seen ? ():null} + {seen ? ():null} ) diff --git a/app/client/src/components/ui/AccountCard.tsx b/app/client/src/components/ui/AccountCard.tsx new file mode 100644 index 0000000..f4898eb --- /dev/null +++ b/app/client/src/components/ui/AccountCard.tsx @@ -0,0 +1,59 @@ +import { deleteUserAccount, type Account } from "../../api/Account"; +import { useState } from "react"; +import AccountPopup from './AccountForm.tsx' + +interface cardProp{ + account:Account + setAccount: (editAccount:Account) => void + removeAccount: (removeAccount:Account) => void +} + + + +export default function card({account, setAccount, removeAccount}:cardProp) { + + //Stores the value that toggles thhe account popup form + const [seen, setSeen] = useState(false) + + const toggle = () => { + setSeen(!seen) + } + + //Deletes the user account from the server and then remove it from the list + const deleteAccount = () => { + + try { + //Send a request to delete user account + deleteUserAccount(account).then((result)=>{ + + //Sucessfully deleted account + if(result) { + removeAccount(account) + } + }) + } catch(error) { + alert("Failed to delete account " + account.name) + } + } + + return( + <> +
+

{account.name}

+

+ +
+

{account.type + " " + account.subtype}

+

{account.balance}

+
+ +
+ + +
+
+ {seen ? ():null} + + ) + +} \ No newline at end of file diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index da7d4ad..e3baf15 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -7,8 +7,8 @@ import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput interface popupProp{ toggle: () => void; - setAccount: (account:Account) => void - addAccount: (account:Account) => void + setAccount?: (account:Account) => void + addAccount?: (account:Account) => void edit: boolean selectedAccount?: Account } @@ -55,7 +55,7 @@ export default function popupForm({toggle, setAccount, addAccount, edit, selecte putUserAccount(newAccount).then((response) =>{ //Determine if sucessfully post - if(response && response.lastUpdated) { + if(response && response.lastUpdated && addAccount) { newAccount.last_updated = response.lastUpdated addAccount(newAccount) } @@ -69,15 +69,18 @@ export default function popupForm({toggle, setAccount, addAccount, edit, selecte //Send a post request to create or edit the account postUserAccount(newAccount).then((response)=>{ - //When creating new account, id is returned for that account + //When creating new account, id and lastupdated is returned for that account if(response && response.id) newAccount.id = response.id - - if(response.lastUpdated) + if(response.lastUpdated){ newAccount.last_updated = response.lastUpdated - setAccount(newAccount) + //Check if setAccount is undefined + if(setAccount){ + setAccount(newAccount) + } + } }) } catch(error) { alert("Failed to create account " + newAccount.name) @@ -113,6 +116,19 @@ export default function popupForm({toggle, setAccount, addAccount, edit, selecte const accountCat: typeofAccount[] = Object.keys(accountCategory) as typeofAccount[] + //Determine if we're editting an account + if(edit && selectedAccount) { + setFormInput({...formInput,["name"]:selectedAccount.name}) + + if(selectedAccount.subtype){ + setFormInput({...formInput, ["subType"]: selectedAccount.subtype}) + } + + setBalance(selectedAccount.balance.toString()) + setAccountType(selectedAccount.type as typeofAccount) + } + + return( <>
diff --git a/app/client/src/components/ui/AccountList.tsx b/app/client/src/components/ui/AccountList.tsx new file mode 100644 index 0000000..fb7f368 --- /dev/null +++ b/app/client/src/components/ui/AccountList.tsx @@ -0,0 +1,54 @@ +import { useState } from "react" +import { getUserAccounts, type Account } from "../../api/Account" +import AccountCard from "./AccountCard" +import AccountPopup from "./AccountForm" + +export default function accountList(){ + + //Stores a lits of the user's account + const [userAccounts, setUserAccounts] = useState([]) + + const [seen, setSeen] = useState(false) + + //Try to get the accounts from the server + try { + getUserAccounts().then((accounts)=>{ + //Determine if we acquired the accounts + if(accounts) { + setUserAccounts(accounts) + } + }) + } catch(error) { + + } + + //Adds a account to the list + const addAccount = (newAccount:Account) => { + setUserAccounts(userAccounts => [...userAccounts,newAccount]) + } + + //Removes any account from the list thats shares the same id + const removeAccount = (remove:Account) => { + setUserAccounts(userAccounts=> userAccounts.filter((account) => account.id !== remove.id)) + } + + const toggle = () => { + setSeen(!seen) + } + + return( + <> +
+
+ {userAccounts.map(account => ())} +
+ +
+ +
+
+ + {seen ? ():null} + + ) +} \ No newline at end of file diff --git a/app/client/src/components/ui/IncomeForm.tsx b/app/client/src/components/ui/IncomeForm.tsx index 6037f53..45f23ec 100644 --- a/app/client/src/components/ui/IncomeForm.tsx +++ b/app/client/src/components/ui/IncomeForm.tsx @@ -6,8 +6,8 @@ import { validateAccountForm, validateIncomeForm } from "../../util/ValidateForm interface popupProp{ toggle: () => void; - setIncome: (income:Income) => void - addIncome: (income:Income) => void + setIncome?: (income:Income) => void + addIncome?: (income:Income) => void edit: boolean selectedIncome?: Income } @@ -23,7 +23,7 @@ export default function popupForm({toggle,setIncome, addIncome, edit, selectedIn if(edit) { putIncome(name,inputAmount,description).then((result) =>{ - if(result && selectedIncome) { + if(result && selectedIncome && setIncome) { const updateIncome: Income = {id:selectedIncome.id, name:name, income:inputAmount, description:description} setIncome(updateIncome) } @@ -38,7 +38,11 @@ export default function popupForm({toggle,setIncome, addIncome, edit, selectedIn if(data && data.id) { newIncome.id = data.id - addIncome(newIncome) + + //Check if add income is defined + if(addIncome){ + addIncome(newIncome) + } } }) @@ -53,24 +57,30 @@ export default function popupForm({toggle,setIncome, addIncome, edit, selectedIn const [amount, setAmount] = useState('') const [description, setDescription] = useState('') + if(edit && selectedIncome) { + setName(selectedIncome.name) + setAmount(selectedIncome.toString()) + setDescription(selectedIncome.description) + } + return( <>
- +
{edit ? (

Edit Income

):(

Create Income

)}

- setName(event.target.value)}/> + setName(event.target.value)}/>

- handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event,amount,setAmount)}/> + handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event,amount,setAmount)}/>

- setDescription(event.target.value)}/> + setDescription(event.target.value)}/>

diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index fced209..da3f060 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -97,8 +97,9 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s //Insert cvs parsing function/validation function } + //State of the user's account + const [account, setAccount] = useState() - //Add when getUserAccounts is implemented getUserAccounts().then(accounts => { console.log(accounts) @@ -108,9 +109,6 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s } }) - //State of the user's account - const [account, setAccount] = useState() - //Holds state of user input const [selectedAccount, setSelectedAccount] = useState("") const [selectedType, setSelectedType] = useState("") @@ -121,7 +119,7 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s const transCat: typeOfTransaction [] = Object.keys(transactionCategory) as typeOfTransaction[]; if(edit && selectedTransaction){ - setSelectedAccount(selectedTransaction.id.toString()) + setSelectedAccount(selectedTransaction.amount.toString()) setSelectedType(selectedTransaction.type) setAmount(selectedTransaction.amount.toString()) } From c791ae0d290bdcb206d4d876210f3b0c930f9dc7 Mon Sep 17 00:00:00 2001 From: Logan Date: Sat, 28 Feb 2026 18:40:48 -0600 Subject: [PATCH 108/204] got auth tests running in dockerfile --- app/server/.env | 2 +- app/server/docker-compose.test.yml | 10 ++++++++++ app/server/services/python/market/Dockerfile | 1 - app/server/services/ts/api-gateway/Dockerfile | 2 +- app/server/services/ts/auth/Dockerfile | 2 +- app/server/services/ts/auth/Dockerfile.test | 10 ++++++++++ app/server/services/ts/bun.lockb | Bin 110427 -> 116880 bytes app/server/services/ts/package.json | 1 + app/server/services/ts/user/Dockerfile | 2 +- app/server/services/ts/vitest.config.ts | 2 +- 10 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 app/server/docker-compose.test.yml create mode 100644 app/server/services/ts/auth/Dockerfile.test diff --git a/app/server/.env b/app/server/.env index aa3d906..314a0ae 100644 --- a/app/server/.env +++ b/app/server/.env @@ -3,7 +3,7 @@ #CI DOCKER_HUB_USERNAME=measureonecodetwice -RELEASE_VERSION=0.0.1 +RELEASE_VERSION=local #DEPLOYMENT MYSQL_ROOT_PASSWORD=dummypw diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml new file mode 100644 index 0000000..974c152 --- /dev/null +++ b/app/server/docker-compose.test.yml @@ -0,0 +1,10 @@ +#This compose file runs all the tests. +#It depends on the main compose file being built +#as all the test images inherit from the regular images. +#you can use the following command to run the tests +#docker compose build -q && docker compose -f docker-compose.test.yml build -q && docker compose -f docker-compose.test.yml up --abort-on-container-failure +services: + auth: + build: + context: services/ts + dockerfile: auth/Dockerfile.test diff --git a/app/server/services/python/market/Dockerfile b/app/server/services/python/market/Dockerfile index cb78353..62b4ba5 100644 --- a/app/server/services/python/market/Dockerfile +++ b/app/server/services/python/market/Dockerfile @@ -1,4 +1,3 @@ -# syntax=docker/dockerfile:1 FROM python:3.11-slim WORKDIR /app diff --git a/app/server/services/ts/api-gateway/Dockerfile b/app/server/services/ts/api-gateway/Dockerfile index ca8751c..d10bcba 100644 --- a/app/server/services/ts/api-gateway/Dockerfile +++ b/app/server/services/ts/api-gateway/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1.0-slim +FROM oven/bun:1.3.10 WORKDIR /project COPY package.json bun.lockb . diff --git a/app/server/services/ts/auth/Dockerfile b/app/server/services/ts/auth/Dockerfile index 9eff0ec..9e00006 100644 --- a/app/server/services/ts/auth/Dockerfile +++ b/app/server/services/ts/auth/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1.0-slim +FROM oven/bun:1.3.10 WORKDIR /project COPY package.json bun.lockb . diff --git a/app/server/services/ts/auth/Dockerfile.test b/app/server/services/ts/auth/Dockerfile.test new file mode 100644 index 0000000..e3de84b --- /dev/null +++ b/app/server/services/ts/auth/Dockerfile.test @@ -0,0 +1,10 @@ +FROM measureonecodetwice/finus-auth:local + +COPY auth/test ./test + +WORKDIR /project +COPY vitest.config.ts . + +ENV CI=true + +CMD [ "bun", "run", "vitest", "--run"] diff --git a/app/server/services/ts/bun.lockb b/app/server/services/ts/bun.lockb index f94c94bebe7541c590b5176a94961ba42e43e4c7..69a8a97c581dd877522b29ff0db90a978cfa6fe9 100755 GIT binary patch delta 18633 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 delta 14746 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 U`ZN}wErQ=-=C1Z{vN?|b21nF?{{R30 diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json index 1ab1f64..74deef1 100644 --- a/app/server/services/ts/package.json +++ b/app/server/services/ts/package.json @@ -7,6 +7,7 @@ "@eslint/css": "^0.14.1", "@eslint/js": "^10.0.1", "@eslint/json": "^1.0.0", + "@vitest/coverage-v8": "4.0.18", "eslint": "^9", "globals": "^17.3.0", "husky": "^9.1.7", diff --git a/app/server/services/ts/user/Dockerfile b/app/server/services/ts/user/Dockerfile index f0abcf8..3948f50 100644 --- a/app/server/services/ts/user/Dockerfile +++ b/app/server/services/ts/user/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1.0-slim +FROM oven/bun:1.3.10 WORKDIR /project COPY package.json bun.lockb . diff --git a/app/server/services/ts/vitest.config.ts b/app/server/services/ts/vitest.config.ts index c55be0d..1a17d0c 100644 --- a/app/server/services/ts/vitest.config.ts +++ b/app/server/services/ts/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config' import path from "path" export default defineConfig({ - resolve : { + resolve: { alias: { "@": path.resolve(__dirname, "./common") } From b8a52bda5894768bf96239c8705486d4505d66a6 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Sat, 28 Feb 2026 18:43:46 -0600 Subject: [PATCH 109/204] Create components to create a list of transactions cards. Transaction form now takes a date of the transaction. --- app/client/src/components/ui/IncomeForm.tsx | 4 +- .../src/components/ui/TransactionCard.tsx | 57 ++++++++++++++++++ .../src/components/ui/TransactionList.tsx | 58 +++++++++++++++++++ .../src/components/ui/TransationForm.tsx | 20 +++++-- app/client/src/util/ValidateForms.ts | 4 +- 5 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 app/client/src/components/ui/TransactionCard.tsx create mode 100644 app/client/src/components/ui/TransactionList.tsx diff --git a/app/client/src/components/ui/IncomeForm.tsx b/app/client/src/components/ui/IncomeForm.tsx index 45f23ec..2c0168d 100644 --- a/app/client/src/components/ui/IncomeForm.tsx +++ b/app/client/src/components/ui/IncomeForm.tsx @@ -1,8 +1,8 @@ import { useState } from "react" import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput" import './UserForm.css' -import {getIncome, postIncome, putIncome, type Income } from "../../api/Income" -import { validateAccountForm, validateIncomeForm } from "../../util/ValidateForms"; +import {postIncome, putIncome, type Income } from "../../api/Income" +import { validateIncomeForm } from "../../util/ValidateForms"; interface popupProp{ toggle: () => void; diff --git a/app/client/src/components/ui/TransactionCard.tsx b/app/client/src/components/ui/TransactionCard.tsx new file mode 100644 index 0000000..10dc360 --- /dev/null +++ b/app/client/src/components/ui/TransactionCard.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { deleteTransaction, type Transaction } from "../../api/Transaction"; +import TransactionPopup from "./TransationForm" + + +interface cardProp{ + transaction: Transaction + setTransaction: (editedTransaction:Transaction) => void + removeTransaction: (removeTransaction:Transaction) => void +} + +export default function card({transaction, setTransaction, removeTransaction}:cardProp) { + + const [seen, setSeen] = useState(false) + + const toggle = () => { + setSeen(!seen) + } + + //Deletes the user account from the server and then remove it from the list + const deleteTrans = () => { + + try { + //Send a request to delete account's transaction + deleteTransaction(transaction).then((result)=>{ + + //Sucessfully deleted transaction + if(result) { + removeTransaction(transaction) + } + }) + } catch(error) { + alert("Failed to delete transaction " + transaction.id) + } + } + + return( + <> +
+

{transaction.id}

+
+

{transaction.amount + " " + transaction.date}

+

+

{transaction.description}

+
+ +
+ + +
+
+ + {seen ? ():null} + + ) + +} \ No newline at end of file diff --git a/app/client/src/components/ui/TransactionList.tsx b/app/client/src/components/ui/TransactionList.tsx new file mode 100644 index 0000000..ac46a78 --- /dev/null +++ b/app/client/src/components/ui/TransactionList.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import { getTransactions, type Transaction } from "../../api/Transaction"; + +import TransactionPopup from "./TransationForm" +import TransactionCard from "./TransactionCard" + +interface listProp{ + accountId: number +} + +export default function transactionList({accountId}:listProp) { + + const [accountTransactions, setAccountTransactions] = useState([]) + const [seen, setSeen] = useState(false) + + + //Try to get the account's transaction from the server + try { + getTransactions(accountId).then((transactions)=>{ + //Determine if we acquired the accounts transaction + if(transactions) { + setAccountTransactions(transactions) + } + }) + } catch(error) { + + } + + //Adds a account to the list + const addTransaction = (newTransaction:Transaction) => { + setAccountTransactions(userTransactions => [...userTransactions,newTransaction]) + } + + //Removes any transaction from the list thats shares the same id + const removeTransaction = (remove:Transaction) => { + setAccountTransactions(userTransactions=> userTransactions.filter((transaction) => transaction.id !== remove.id)) + } + + const toggle = () => { + setSeen(!seen) + } + + return( + <> +
+
+ {accountTransactions.map(transaction => ())} +
+ +
+ +
+
+ + {seen ? ():null} + + ) +} \ No newline at end of file diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index da3f060..5741a0d 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -8,8 +8,8 @@ import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput interface popupProp{ toggle: () => void; - setTransaction: (editedTransaction:Transaction) => void - addTransaction: (newTransaction:Transaction) => void + setTransaction?: (editedTransaction:Transaction) => void + addTransaction?: (newTransaction:Transaction) => void edit: boolean selectedTransaction?: Transaction } @@ -36,9 +36,9 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s const transferAmount = Number(amount) let userTransaction: Transaction - if(selectedType && validateTransactionForm(accountId, selectedType, transferAmount, file)){ + if(selectedType && validateTransactionForm(accountId, selectedType, transferAmount, selectedDate, file)){ - userTransaction = {id: 0, financialAccount_id:accountId, amount:transferAmount, type: selectedType, date: new Date()} + userTransaction = {id: 0, financialAccount_id:accountId, amount:transferAmount, type: selectedType, date: new Date(selectedDate)} if(edit && selectedTransaction) { try{ @@ -48,7 +48,7 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s //Send a request to update the transaction putTranscations(userTransaction).then((result) => { //Determine if the - if(result) { + if(result && setTransaction) { setTransaction(userTransaction) } }) @@ -64,7 +64,10 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s //Successful put if response is returned if(response && response[0].id) { userTransaction.id = response[0].id - addTransaction(userTransaction) + + if(addTransaction){ + addTransaction(userTransaction) + } } else { alert("Failed to create transaction") } @@ -114,6 +117,7 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s const [selectedType, setSelectedType] = useState("") const [amount, setAmount] = useState("") const [file, setFile] = useState(undefined) + const [selectedDate, setSelectedDate] = useState("") //Holds the types of transfers const transCat: typeOfTransaction [] = Object.keys(transactionCategory) as typeOfTransaction[]; @@ -150,6 +154,10 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} placeholder="0.00"/>

+ + setSelectedDate(event.target.value)}/> +

+ {edit ? (null):( <> diff --git a/app/client/src/util/ValidateForms.ts b/app/client/src/util/ValidateForms.ts index 8a06908..3a3e1e8 100644 --- a/app/client/src/util/ValidateForms.ts +++ b/app/client/src/util/ValidateForms.ts @@ -22,7 +22,7 @@ export function validateAccountForm(name:string, accountType:string, balance: nu return result } -export function validateTransactionForm(account_id:number, transactionType:string, amount:number, csvFile?:File) { +export function validateTransactionForm(account_id:number, transactionType:string, amount:number,date:string, csvFile?:File) { let result = false @@ -33,7 +33,7 @@ export function validateTransactionForm(account_id:number, transactionType:strin if(csvFile) { result = true - } else if(Object.values(transactionCategory).includes(transactionType) && amount > 0) { + } else if(Object.values(transactionCategory).includes(transactionType) && amount > 0 && date) { result = true } } From e610dd05b326a9b55912a474fe4c2ac530c234c5 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Sat, 28 Feb 2026 23:03:50 -0600 Subject: [PATCH 110/204] Created components to create a list of user's income with cards. --- app/client/src/components/ui/IncomeCard.tsx | 56 +++++++++++++++++++++ app/client/src/components/ui/IncomeList.tsx | 53 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 app/client/src/components/ui/IncomeCard.tsx create mode 100644 app/client/src/components/ui/IncomeList.tsx diff --git a/app/client/src/components/ui/IncomeCard.tsx b/app/client/src/components/ui/IncomeCard.tsx new file mode 100644 index 0000000..816db1d --- /dev/null +++ b/app/client/src/components/ui/IncomeCard.tsx @@ -0,0 +1,56 @@ +import { useState } from "react" +import { deleteIncome, type Income } from "../../api/Income" +import IncomePopup from "./IncomeForm" + +interface cardProp{ + income: Income + setIncome: (editIncome:Income) => void + removeIncome: (removeIncome:Income) => void +} + +export default function card({income, setIncome, removeIncome}:cardProp) { + + const [seen, setSeen] = useState(false) + + const toggle = () => { + setSeen(!seen) + } + + //Deletes the user income from the server and then remove it from the list + const deleteUserIncome = () => { + + try { + //Send a request to delete user's income + deleteIncome(income).then((result)=>{ + + //Sucessfully deleted income + if(result) { + removeIncome(income) + } + }) + } catch(error) { + alert("Failed to delete income " + income.name) + } + } + + return( + <> +
+

{income.name}

+
+

{income.income}

+

+

{income.description}

+
+ +
+ + +
+
+ + {seen ? ():null} + + ) + +} \ No newline at end of file diff --git a/app/client/src/components/ui/IncomeList.tsx b/app/client/src/components/ui/IncomeList.tsx new file mode 100644 index 0000000..ac75988 --- /dev/null +++ b/app/client/src/components/ui/IncomeList.tsx @@ -0,0 +1,53 @@ +import { useState } from "react" +import { getIncome, type Income } from "../../api/Income" +import IncomePopup from "./IncomeForm" +import IncomeCard from "./IncomeCard" + +export default function IncomeList() { + + const [userIncomes, setUsersIncomes] = useState([]) + const [seen, setSeen] = useState(false) + + + //Try to get the account's transaction from the server + try { + getIncome().then((incomes)=>{ + //Determine if we acquired the accounts transaction + if(incomes) { + setUsersIncomes(incomes) + } + }) + } catch(error) { + + } + + //Adds a account to the list + const addIncome = (newIncome:Income) => { + setUsersIncomes(userIncomes => [...userIncomes,newIncome]) + } + + //Removes any income from the list thats shares the same id + const removeIncome = (remove:Income) => { + setUsersIncomes(userIncomes=> userIncomes.filter((incomes) => incomes.id !== remove.id)) + } + + const toggle = () => { + setSeen(!seen) + } + + return( + <> +
+
+ {userIncomes.map(income => ())} +
+ +
+ +
+
+ + {seen ? ():null} + + ) +} \ No newline at end of file From 085a223e7dbc2621e4e83cfe6e3f509d6ae2c2ed Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Sun, 1 Mar 2026 01:34:05 -0600 Subject: [PATCH 111/204] Cleaned up code, put the object types into their own file. --- app/client/src/api/Account.ts | 12 +----------- app/client/src/api/Income.ts | 10 ++-------- app/client/src/api/Transaction.ts | 15 +-------------- app/client/src/components/ui/AccountCard.tsx | 3 ++- app/client/src/components/ui/AccountForm.tsx | 4 +++- app/client/src/components/ui/AccountList.tsx | 4 +++- app/client/src/components/ui/IncomeCard.tsx | 3 ++- app/client/src/components/ui/IncomeForm.tsx | 3 ++- app/client/src/components/ui/IncomeList.tsx | 3 ++- app/client/src/components/ui/TransactionCard.tsx | 3 ++- app/client/src/components/ui/TransactionList.tsx | 4 ++-- app/client/src/components/ui/TransationForm.tsx | 8 +++++--- app/client/src/type/AccountType.ts | 10 ++++++++++ app/client/src/type/IncomeType.ts | 7 +++++++ app/client/src/type/TransactionType.ts | 11 +++++++++++ 15 files changed, 55 insertions(+), 45 deletions(-) create mode 100644 app/client/src/type/AccountType.ts create mode 100644 app/client/src/type/IncomeType.ts create mode 100644 app/client/src/type/TransactionType.ts diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index ecc9f90..8769e28 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,15 +1,5 @@ import type { updateResponse } from "../type/responseTypes"; - -//Coppied from module -export interface Account{ - id: number; - name: string; - type: string; - balance: number; - value: number; - subtype?: string; - last_updated: Date; -} +import type { Account } from "../type/AccountType"; const requestUrl = 'http://localhost:3000/accounts' diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts index a29613f..3e61908 100644 --- a/app/client/src/api/Income.ts +++ b/app/client/src/api/Income.ts @@ -1,12 +1,6 @@ -import { resumeAndPrerenderToNodeStream } from "react-dom/static" -import type { updateResponse } from "../type/responseTypes" -export interface Income { - id:number, - name:string, - income: number, - description: string -} +import type { updateResponse } from "../type/responseTypes" +import { type Income } from "../type/IncomeType" const requestUrl = "http://localhost:3000/income" diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 89d5b3b..9591e8d 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,18 +1,5 @@ -import type { promises } from "dns"; -import { get, request } from "http"; -import { json } from "stream/consumers"; import type { updateResponse } from "../type/responseTypes"; - -export interface Transaction{ - id: number; - financialAccount_id: number; - amount: number; - type: string - description?: string; - sender?: string; - recipient?: string; - date: Date; -} +import { type Transaction } from "../type/TransactionType"; const requestUrl = "http://localhost:3000/transacitons" diff --git a/app/client/src/components/ui/AccountCard.tsx b/app/client/src/components/ui/AccountCard.tsx index f4898eb..6aaf670 100644 --- a/app/client/src/components/ui/AccountCard.tsx +++ b/app/client/src/components/ui/AccountCard.tsx @@ -1,6 +1,7 @@ -import { deleteUserAccount, type Account } from "../../api/Account"; +import { deleteUserAccount } from "../../api/Account"; import { useState } from "react"; import AccountPopup from './AccountForm.tsx' +import { type Account } from "../../type/AccountType.ts"; interface cardProp{ account:Account diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index e3baf15..cb3f9e3 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,10 +1,12 @@ import React, { useState } from "react"; import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' -import {postUserAccount, putUserAccount, type Account} from '../../api/Account.ts' +import {postUserAccount, putUserAccount} from '../../api/Account.ts' +import { type Account } from "../../type/AccountType.ts"; import { validateAccountForm } from "../../util/ValidateForms.ts"; import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput.ts"; + interface popupProp{ toggle: () => void; setAccount?: (account:Account) => void diff --git a/app/client/src/components/ui/AccountList.tsx b/app/client/src/components/ui/AccountList.tsx index fb7f368..76056d0 100644 --- a/app/client/src/components/ui/AccountList.tsx +++ b/app/client/src/components/ui/AccountList.tsx @@ -1,8 +1,10 @@ import { useState } from "react" -import { getUserAccounts, type Account } from "../../api/Account" +import { getUserAccounts} from "../../api/Account" +import { type Account } from "../../type/AccountType" import AccountCard from "./AccountCard" import AccountPopup from "./AccountForm" + export default function accountList(){ //Stores a lits of the user's account diff --git a/app/client/src/components/ui/IncomeCard.tsx b/app/client/src/components/ui/IncomeCard.tsx index 816db1d..4d56258 100644 --- a/app/client/src/components/ui/IncomeCard.tsx +++ b/app/client/src/components/ui/IncomeCard.tsx @@ -1,5 +1,6 @@ import { useState } from "react" -import { deleteIncome, type Income } from "../../api/Income" +import { deleteIncome} from "../../api/Income" +import { type Income } from "../../type/IncomeType" import IncomePopup from "./IncomeForm" interface cardProp{ diff --git a/app/client/src/components/ui/IncomeForm.tsx b/app/client/src/components/ui/IncomeForm.tsx index 2c0168d..5b9defc 100644 --- a/app/client/src/components/ui/IncomeForm.tsx +++ b/app/client/src/components/ui/IncomeForm.tsx @@ -1,8 +1,9 @@ import { useState } from "react" import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput" import './UserForm.css' -import {postIncome, putIncome, type Income } from "../../api/Income" +import {postIncome, putIncome} from "../../api/Income" import { validateIncomeForm } from "../../util/ValidateForms"; +import { type Income } from "../../type/IncomeType"; interface popupProp{ toggle: () => void; diff --git a/app/client/src/components/ui/IncomeList.tsx b/app/client/src/components/ui/IncomeList.tsx index ac75988..7bea7a7 100644 --- a/app/client/src/components/ui/IncomeList.tsx +++ b/app/client/src/components/ui/IncomeList.tsx @@ -1,5 +1,6 @@ import { useState } from "react" -import { getIncome, type Income } from "../../api/Income" +import { getIncome} from "../../api/Income" +import { type Income } from "../../type/IncomeType" import IncomePopup from "./IncomeForm" import IncomeCard from "./IncomeCard" diff --git a/app/client/src/components/ui/TransactionCard.tsx b/app/client/src/components/ui/TransactionCard.tsx index 10dc360..f527ad9 100644 --- a/app/client/src/components/ui/TransactionCard.tsx +++ b/app/client/src/components/ui/TransactionCard.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { deleteTransaction, type Transaction } from "../../api/Transaction"; +import { deleteTransaction} from "../../api/Transaction"; +import { type Transaction } from "../../type/TransactionType"; import TransactionPopup from "./TransationForm" diff --git a/app/client/src/components/ui/TransactionList.tsx b/app/client/src/components/ui/TransactionList.tsx index ac46a78..a786e2d 100644 --- a/app/client/src/components/ui/TransactionList.tsx +++ b/app/client/src/components/ui/TransactionList.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { getTransactions, type Transaction } from "../../api/Transaction"; - +import { getTransactions} from "../../api/Transaction"; +import { type Transaction } from "../../type/TransactionType"; import TransactionPopup from "./TransationForm" import TransactionCard from "./TransactionCard" diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index 5741a0d..8ae430a 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -1,10 +1,12 @@ -import React, { useState, type ReactHTMLElement } from "react"; +import React, { useState} from "react"; import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' -import { getUserAccounts, type Account } from "../../api/Account"; +import { getUserAccounts} from "../../api/Account"; import { validateTransactionForm } from "../../util/ValidateForms"; -import { postTranscations, putTranscations, type Transaction } from "../../api/Transaction"; +import { postTranscations, putTranscations} from "../../api/Transaction"; import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput"; +import { type Transaction } from "../../type/TransactionType"; +import { type Account } from "../../type/AccountType"; interface popupProp{ toggle: () => void; diff --git a/app/client/src/type/AccountType.ts b/app/client/src/type/AccountType.ts new file mode 100644 index 0000000..85f3648 --- /dev/null +++ b/app/client/src/type/AccountType.ts @@ -0,0 +1,10 @@ +//Coppied from Finance-Account type from server +export interface Account{ + id: number; + name: string; + type: string; + balance: number; + value: number; + subtype?: string; + last_updated: Date; +} \ No newline at end of file diff --git a/app/client/src/type/IncomeType.ts b/app/client/src/type/IncomeType.ts new file mode 100644 index 0000000..de80e19 --- /dev/null +++ b/app/client/src/type/IncomeType.ts @@ -0,0 +1,7 @@ + +export interface Income { + id:number, + name:string, + income: number, + description: string +} diff --git a/app/client/src/type/TransactionType.ts b/app/client/src/type/TransactionType.ts new file mode 100644 index 0000000..a13e492 --- /dev/null +++ b/app/client/src/type/TransactionType.ts @@ -0,0 +1,11 @@ +//Coppied from server side transaction type +export interface Transaction{ + id: number; + financialAccount_id: number; + amount: number; + type: string + description?: string; + sender?: string; + recipient?: string; + date: Date; +} From 56c1e5bdb01edc70d7165f85d8a2fb1b421d0a6a Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Sun, 1 Mar 2026 10:31:19 -0600 Subject: [PATCH 112/204] implemented a csv Success model and updated all relevant forms to reflect this --- .../src/components/csvread/CsvUpload.tsx | 104 +++++++++++------- .../src/components/csvread/csvSuccess.tsx | 22 ++++ .../src/components/ui/TransationForm.tsx | 9 +- 3 files changed, 91 insertions(+), 44 deletions(-) create mode 100644 app/client/src/components/csvread/csvSuccess.tsx diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index 5fa55f7..3fa96d6 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -8,13 +8,15 @@ import CsvPreviewTable from "./CsvPreview"; import CsvConfirmation from "./CsvConfirm"; import { parseCsvFile } from "../../util/ParseCsv"; import type { TransactionDraft } from "../../util/ConvertTransaction"; +import SuccessScreen from "./csvSuccess"; -export default function CsvUpload() { +export default function CsvUpload({ accountId }: { accountId: number }) { const [file, setFile] = useState(null); // selected CSV file const [error, setError] = useState(null); // validation errors const [parsedData, setParsedData] = useState(null); // state for parsed and validated data const [isParsing, setIsParsing] = useState(false); // track if parsing is in progress const [showConfirmation, setShowConfirmation] = useState(false); + const [importResult, setImportResult] = useState(null); @@ -78,6 +80,9 @@ export default function CsvUpload() { setIsParsing(false); }; + + + //handle import sends valid transactions to backend for insertion into database const handleImport = async () => { if (!parsedData) return; @@ -87,7 +92,7 @@ const handleImport = async () => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - financialAccount_id: 1, // TODO: replace with real account ID + financialAccount_id: accountId, transactions: parsedData }), }); @@ -99,10 +104,14 @@ const handleImport = async () => { return; } - console.log("Import success:", result); - // TODO: show success UI or redirect - } catch (err) { - setError("Network error during import"); + setImportResult({ + inserted: result.inserted, + skipped: result.skipped + }); + + } catch (error){ + console.error("Import failed:", error); + setError("Error during import"); } }; @@ -110,45 +119,60 @@ const handleImport = async () => { return (
- - - {error && } - - {file && } - {/* only show preview button if we have a file and we're not already in confirmation step */} - {!showConfirmation && ( - + {importResult && ( + { + setImportResult(null); + setParsedData(null); + setFile(null); + setShowConfirmation(false); + }} + /> )} - - {/* show preview table and continue button if we have parsed data and we're not in confirmation step */} - {parsedData && parsedData.length > 0 && !showConfirmation && ( + {!importResult && ( <> - - - + + + {error && } + + {file && } + + {!showConfirmation && ( + + )} + + {parsedData && parsedData.length > 0 && !showConfirmation && ( + <> + + + + + )} + + {parsedData && parsedData.length > 0 && showConfirmation && ( + setShowConfirmation(false)} + onConfirm={handleImport} + /> + )} )} - - {/* show confirmation step */} - {parsedData && parsedData.length > 0 && showConfirmation && ( - setShowConfirmation(false)} - onConfirm={handleImport} // send to backend - /> - )}
); + } \ No newline at end of file diff --git a/app/client/src/components/csvread/csvSuccess.tsx b/app/client/src/components/csvread/csvSuccess.tsx new file mode 100644 index 0000000..f03ff15 --- /dev/null +++ b/app/client/src/components/csvread/csvSuccess.tsx @@ -0,0 +1,22 @@ +//util function to show success message after import +export default function SuccessScreen({ + inserted, + skipped, + onDone +}: { + inserted: number; + skipped: number; + onDone: () => void; +}) { + return ( +
+

Import Complete

+

{inserted} transactions imported successfully.

+

{skipped} rows were skipped due to validation errors.

+ + +
+ ); +} diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index 5741a0d..dadca7a 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -138,7 +138,7 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s

@@ -160,11 +160,12 @@ export default function popupForm({toggle,setTransaction,addTransaction, edit, s {edit ? (null):( <> - + + {!edit && selectedAccount && + ( ) + } )} - -

From f2d33aaaa4d052e937525e7a86423e364025b07c Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Sun, 1 Mar 2026 16:06:18 -0600 Subject: [PATCH 113/204] Temp removed (csv upload from accounts) --- app/client/src/components/ui/AccountForm.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index e3baf15..5ca85a9 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' import {postUserAccount, putUserAccount, type Account} from '../../api/Account.ts' import { validateAccountForm } from "../../util/ValidateForms.ts"; @@ -152,10 +151,6 @@ export default function popupForm({toggle, setAccount, addAccount, edit, selecte handleCurrencyChange(event, setBalance)} onBlur ={(event) => handleCurrencyBlur(event,balance,setBalance)} placeholder="0.00"/>

- - -

- {(accountType === "DEBT" || accountType === "SAVING") ? ( <> From 011db2e4ac9535e49817e72a6fc89c9d30bb42cb Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 1 Mar 2026 12:30:19 -0600 Subject: [PATCH 114/204] refactor workflow image publishing --- .github/workflows/docker_compose_publish.yml | 41 +++++++++++++++ .github/workflows/publish_backend_images.yml | 53 ++++++-------------- 2 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/docker_compose_publish.yml diff --git a/.github/workflows/docker_compose_publish.yml b/.github/workflows/docker_compose_publish.yml new file mode 100644 index 0000000..7e43184 --- /dev/null +++ b/.github/workflows/docker_compose_publish.yml @@ -0,0 +1,41 @@ +on: + workflow_call: + inputs: + dockerfile-dir: + type: string + required: true + DOCKER_HUB_USERNAME: + type: string + required: true + + secrets: + DOCKER_HUB_PASSWORD: + required: true + +jobs: + publish-compose: + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: ${{ inputs.dockerfile-dir }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: docker/setup-buildx-action@000d75d273dec231f74115df50b6ee04c9b25e55 + - uses: docker/login-action@0d33dcdeedfe8f116d005eaa548b93b404f61fa8 + with: + username: ${{ inputs.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + - run: | + BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + RELEASE_VERSION=$(echo $BRANCH_NAME | sed "s|release\/||g") + + echo RELEASE_VERSION=$RELEASE_VERSION > .env + echo DOCKER_HUB_USERNAME=$DOCKER_HUB_USERNAME >> .env + + echo "Building images..." + docker compose build + echo "Pushing images..." + docker compose push + env: + DOCKER_HUB_USERNAME: ${{ inputs.DOCKER_HUB_USERNAME }} + diff --git a/.github/workflows/publish_backend_images.yml b/.github/workflows/publish_backend_images.yml index c0e45ce..269a21a 100644 --- a/.github/workflows/publish_backend_images.yml +++ b/.github/workflows/publish_backend_images.yml @@ -1,4 +1,4 @@ -name: "Publish Backend Images" +name: "Publish Images" on: push: @@ -7,42 +7,21 @@ on: workflow_dispatch: jobs: - publish-backend: - runs-on: ubuntu-24.04 - environment: "Docker hub" - defaults: - run: - working-directory: app/server - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - uses: docker/setup-buildx-action@000d75d273dec231f74115df50b6ee04c9b25e55 - - name: setup - run: | - BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} - RELEASE_VERSION=$(echo $BRANCH_NAME | sed "s|release\/||g") - echo RELEASE_VERSION=$RELEASE_VERSION > .env - echo DOCKER_HUB_USERNAME=${{ vars.DOCKER_HUB_USERNAME }} >> .env + publish-backend-images: + uses: ./.github/workflows/docker_compose_publish.yml + with: + dockerfile-dir: app/server + DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME }} + secrets: + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ vars.DOCKER_HUB_USERNAME }} --password-stdin - docker compose build -q - docker compose push -q + publish-frontend-images: + name: "Publish frontend images" + uses: ./.github/workflows/docker_compose_publish.yml + with: + dockerfile-dir: app/client + DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME }} + secrets: + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - publish-frontend: - runs-on: ubuntu-24.04 - environment: "Docker hub" - defaults: - run: - working-directory: app/client - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - uses: docker/setup-buildx-action@000d75d273dec231f74115df50b6ee04c9b25e55 - - name: setup - run: | - BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} - RELEASE_VERSION=$(echo $BRANCH_NAME | sed "s|release\/||g") - echo RELEASE_VERSION=$RELEASE_VERSION > .env - echo DOCKER_HUB_USERNAME=${{ vars.DOCKER_HUB_USERNAME }} >> .env - echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ vars.DOCKER_HUB_USERNAME }} --password-stdin - docker compose build -q - docker compose push -q From 127be71a6794e46c8dabf0ccd6765f8cadbdbc00 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 1 Mar 2026 19:04:43 -0600 Subject: [PATCH 115/204] fixed issue where would check every file if none changed --- hooks/format_ts_dir.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hooks/format_ts_dir.sh b/hooks/format_ts_dir.sh index 0795180..f9168c9 100755 --- a/hooks/format_ts_dir.sh +++ b/hooks/format_ts_dir.sh @@ -1,6 +1,10 @@ #!/bin/bash #first argument ($1) is the directory to format the files of. changed_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E $1/) +if [[ $changed_files == '' ]]; then + exit 0 +fi + normalized_files=$(echo $changed_files | sed "s|$1/||g" ) cd $1 bun eslint $changed_dir_files From 543004e166504cd3a147c2912a34a564cb7d12f2 Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 2 Mar 2026 11:40:13 -0600 Subject: [PATCH 116/204] Added workflow to run tests. Runs on every pull request and pushes to main, dev and release branches --- .github/workflows/run_tests.yml | 26 ++++++++++++++++++++++++++ app/server/docker-compose.test.yml | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/run_tests.yml diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 0000000..0407fde --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,26 @@ +name: "Run tests" + +on: + pull_request: + branches: [ "*" ] + push: + branches: [ "main", "development", "release/*" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + run-backend-tests: + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: app/server + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Build base images + run: docker compose build + - name: Build test images + run: | + mv docker-compose.test.yml docker-compose.yml + docker compose build + - name: Run tests + run: docker compose up --abort-on-container-failure diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 974c152..45368ce 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -4,7 +4,7 @@ #you can use the following command to run the tests #docker compose build -q && docker compose -f docker-compose.test.yml build -q && docker compose -f docker-compose.test.yml up --abort-on-container-failure services: - auth: + auth-test: build: context: services/ts dockerfile: auth/Dockerfile.test From 7f7ec870ffb8ec21d5cc183d6c360b6a8ace4d40 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Mon, 2 Mar 2026 16:42:13 -0600 Subject: [PATCH 117/204] Fixed 404 error, move some of the file around so that user service is handling /accounts, /transactions, and /profiles requests instead of database --- .gitignore | 1 + app/client/src/App.tsx | 4 +- app/client/src/api/Account.ts | 7 +- app/client/src/api/Income.ts | 2 +- app/client/src/api/Transaction.ts | 2 +- app/client/src/components/ui/AccountList.tsx | 4 +- .../{AccountCard.tsx => AccountListCard.tsx} | 2 +- app/server/default.env | 5 + app/server/services/api-gateway/bun.lock | 412 ++++++++++++++++++ app/server/services/api-gateway/src/index.ts | 31 +- .../services/database/src/db/connection.ts | 2 +- .../services/database/src/routes/account.ts | 4 +- .../services/database/src/routes/profile.ts | 1 + app/server/services/user/package.json | 3 +- app/server/services/user/src/db/connection.ts | 14 + app/server/services/user/src/index.ts | 22 +- .../services/user/src/routes/account.ts | 34 ++ .../services/user/src/routes/profile.ts | 30 ++ .../services/user/src/routes/transaction.ts | 107 +++++ 19 files changed, 655 insertions(+), 32 deletions(-) rename app/client/src/components/ui/{AccountCard.tsx => AccountListCard.tsx} (96%) create mode 100644 app/server/services/user/src/db/connection.ts create mode 100644 app/server/services/user/src/routes/account.ts create mode 100644 app/server/services/user/src/routes/profile.ts create mode 100644 app/server/services/user/src/routes/transaction.ts diff --git a/.gitignore b/.gitignore index b7faf40..856441d 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,4 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ +app/server/services/api-gateway/bun.lock diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index e1064b5..b549985 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -9,7 +9,7 @@ import LoadingSpinner from './components/ui/LoadingSpinner.tsx' import AccountPopup from './components/ui/AccountForm.tsx' import TransactionPopup from './components/ui/TransationForm.tsx' import IncomePopup from './components/ui/IncomeForm.tsx' -import type { Account } from './api/Account.ts' +import type { Account } from './type/AccountType.ts' function App() { return ( @@ -76,7 +76,7 @@ function NotFoundPage() {

- {seen ? ():null} + {seen ? ():null} ) diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index 8769e28..67ce2d7 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,7 +1,7 @@ import type { updateResponse } from "../type/responseTypes"; import type { Account } from "../type/AccountType"; -const requestUrl = 'http://localhost:3000/accounts' +const requestUrl = 'http://localhost:3000/api/accounts' //Sends a request to get different accounts the user has export async function getUserAccounts(): Promise { @@ -10,9 +10,6 @@ export async function getUserAccounts(): Promise { //Sends a http request and waits for a response const response = await fetch(requestUrl, { method: 'GET', - headers: { - 'Cookie': document.cookie - }, credentials: "include" }) @@ -38,7 +35,7 @@ export async function postUserAccount(newAccount:Account):Promise { diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 9591e8d..eaacd4e 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,7 +1,7 @@ import type { updateResponse } from "../type/responseTypes"; import { type Transaction } from "../type/TransactionType"; -const requestUrl = "http://localhost:3000/transacitons" +const requestUrl = "http://localhost:3000/api/transacitons" //Sends a GET request to get the list of user transactions for the account export async function getTransactions(account_id: number): Promise{ diff --git a/app/client/src/components/ui/AccountList.tsx b/app/client/src/components/ui/AccountList.tsx index 76056d0..9bab281 100644 --- a/app/client/src/components/ui/AccountList.tsx +++ b/app/client/src/components/ui/AccountList.tsx @@ -1,7 +1,7 @@ import { useState } from "react" import { getUserAccounts} from "../../api/Account" import { type Account } from "../../type/AccountType" -import AccountCard from "./AccountCard" +import AccountListCard from "./AccountListCard" import AccountPopup from "./AccountForm" @@ -42,7 +42,7 @@ export default function accountList(){ <>
- {userAccounts.map(account => ())} + {userAccounts.map(account => ())}
diff --git a/app/client/src/components/ui/AccountCard.tsx b/app/client/src/components/ui/AccountListCard.tsx similarity index 96% rename from app/client/src/components/ui/AccountCard.tsx rename to app/client/src/components/ui/AccountListCard.tsx index 6aaf670..16add3d 100644 --- a/app/client/src/components/ui/AccountCard.tsx +++ b/app/client/src/components/ui/AccountListCard.tsx @@ -1,4 +1,4 @@ -import { deleteUserAccount } from "../../api/Account"; +import { deleteUserAccount } from "../../api/Account.ts"; import { useState } from "react"; import AccountPopup from './AccountForm.tsx' import { type Account } from "../../type/AccountType.ts"; diff --git a/app/server/default.env b/app/server/default.env index d08498c..772b5e8 100644 --- a/app/server/default.env +++ b/app/server/default.env @@ -3,3 +3,8 @@ AUTH_SERVICE_ADDR=http://auth:8000 USER_SERVICE_ADDR=http://user:8000 ANALYTICS_SERVICE_ADDR=http://analytics:8000 MARKET_SERVICE_ADDR=http://market:8000 +DATABASE_SERVICE_ADDR=http://database:3000 + +MYSQL_HOST=database +MYSQL_PORT=3306 +DB_NAME=finus diff --git a/app/server/services/api-gateway/bun.lock b/app/server/services/api-gateway/bun.lock index 8bff853..ca1bb46 100644 --- a/app/server/services/api-gateway/bun.lock +++ b/app/server/services/api-gateway/bun.lock @@ -8,17 +8,51 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "cors": "^2.8.6", + "eslint": "^9.39.2", "express": "^5.2.1", "http-proxy-middleware": "^3.0.5", }, "devDependencies": { + "@eslint/js": "^9.39.2", "@types/bun": "latest", "@types/node": "^25.2.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "eslint-plugin-react": "^7.37.5", + "globals": "^17.3.0", + "jiti": "^2.6.1", "typescript": "^5.3.3", + "typescript-eslint": "^8.54.0", }, }, }, "packages": { + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], + + "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], @@ -27,6 +61,8 @@ "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], @@ -35,6 +71,8 @@ "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -45,20 +83,84 @@ "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -69,50 +171,138 @@ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], @@ -123,20 +313,102 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], @@ -149,44 +421,112 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -197,20 +537,92 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/app/server/services/api-gateway/src/index.ts b/app/server/services/api-gateway/src/index.ts index 3c2d3f8..d6a370b 100644 --- a/app/server/services/api-gateway/src/index.ts +++ b/app/server/services/api-gateway/src/index.ts @@ -22,12 +22,11 @@ app.use(cors({ allowedHeaders: ['Content-Type', 'Authorization'] })); -app.use(express.json()); -const server = app.listen(PORT, () => { - console.log(`API Gateway running on port ${PORT}`); -}); -process.on("SIGTERM", () => server.close()); - +app.use((req,res,next)=>{ + console.log("API-GATEWAY Incoming request: " + req.method + " " + req.url) + console.log(req.body) + next() +}) //test endpoint app.get('/health', async (req: express.Request, res: express.Response) => { @@ -47,28 +46,34 @@ process.on("SIGTERM", () => server.close()); // Api Gateway routes to account, transaction and profiles app.use( - "/api/accounts", createProxyMiddleware({ - target: process.env.DATABASE_SERVICE_ADDR, + pathFilter: "/api/accounts", + target: process.env.USER_SERVICE_ADDR, changeOrigin: true, - pathRewrite: { "^/api/accounts": "/accounts" } + pathRewrite: { "^/api/accounts": "/accounts" }, }) ); app.use( - "/api/transactions", createProxyMiddleware({ - target: process.env.DATABASE_SERVICE_ADDR, + pathFilter: "/api/tranasctions", + target: process.env.USER_SERVICE_ADDR, changeOrigin: true, pathRewrite: { "^/api/transactions": "/transactions" } }) ); app.use( - "/api/profiles", createProxyMiddleware({ - target: process.env.DATABASE_SERVICE_ADDR, + pathFilter:"/api/profiles", + target: process.env.USER_SERVICE_ADDR, changeOrigin: true, pathRewrite: { "^/api/profiles": "/profiles" } }) ); + +app.use(express.json()); +const server = app.listen(PORT, () => { + console.log(`API Gateway running on port ${PORT}`); +}); +process.on("SIGTERM", () => server.close()); \ No newline at end of file diff --git a/app/server/services/database/src/db/connection.ts b/app/server/services/database/src/db/connection.ts index 9699aa7..151f0b5 100644 --- a/app/server/services/database/src/db/connection.ts +++ b/app/server/services/database/src/db/connection.ts @@ -8,7 +8,7 @@ export const db = mysql.createPool({ port: 3306, user: 'root', password: 'a', - database: "Finus" + database: "finus" }); diff --git a/app/server/services/database/src/routes/account.ts b/app/server/services/database/src/routes/account.ts index b50bc78..e6a32ce 100644 --- a/app/server/services/database/src/routes/account.ts +++ b/app/server/services/database/src/routes/account.ts @@ -21,11 +21,13 @@ accountsRouter.post("/", async (req: Request, res: Response) => { res.json({ message: "Account successfully created", id: result.insertId, + lastUpdated: new Date() }); - res.append("Access-Control-Allow-Origin", "http://localhost:8080/") + } catch (err) { console.error("Account creation failed", err); res.status(500).json({ error: "Account creation failed" }); } }); + diff --git a/app/server/services/database/src/routes/profile.ts b/app/server/services/database/src/routes/profile.ts index df3060a..e811bb5 100644 --- a/app/server/services/database/src/routes/profile.ts +++ b/app/server/services/database/src/routes/profile.ts @@ -27,3 +27,4 @@ profilesRouter.post("/", async (req: Request, res: Response) => { res.status(500).json({ error: "Profile creation failed" }); } }); + diff --git a/app/server/services/user/package.json b/app/server/services/user/package.json index a236d31..9b2541a 100644 --- a/app/server/services/user/package.json +++ b/app/server/services/user/package.json @@ -20,7 +20,8 @@ "typescript": "^5.9.3" }, "dependencies": { - "express": "^5.2.1" + "express": "^5.2.1", + "mysql2": "^3.6.0" }, "scripts": { "dev": "bun --watch src/index.ts", diff --git a/app/server/services/user/src/db/connection.ts b/app/server/services/user/src/db/connection.ts new file mode 100644 index 0000000..151f0b5 --- /dev/null +++ b/app/server/services/user/src/db/connection.ts @@ -0,0 +1,14 @@ +// TODO: create connection and export it +import mysql from "mysql2/promise" + +export const db = mysql.createPool({ + + //hard coded values + host: 'database', + port: 3306, + user: 'root', + password: 'a', + database: "finus" + +}); + diff --git a/app/server/services/user/src/index.ts b/app/server/services/user/src/index.ts index 8152996..bd02927 100644 --- a/app/server/services/user/src/index.ts +++ b/app/server/services/user/src/index.ts @@ -1,15 +1,29 @@ import express from 'express'; +import { accountsRouter } from './routes/account'; +import { profilesRouter } from './routes/profile'; +import { transactionsRouter } from './routes/transaction'; + const app = express(); const PORT = process.env.PORT || 8000; app.use(express.json()); -const server = app.listen(PORT, () => { - console.log(`User Service running on port ${PORT}`); -}); -process.on("SIGTERM", () => server.close()); +app.use((req,res,next)=>{ + console.log("USER Incoming request: " + req.method + " " + req.url) + console.log(req.body) + next() +}) //test endpoint app.get('/health', (req: express.Request, res: express.Response) => { res.send('ok'); }); + +app.use("/accounts", accountsRouter); +app.use("/transactions", transactionsRouter); +app.use("/profiles", profilesRouter); + +const server = app.listen(PORT, () => { + console.log(`User Service running on port ${PORT}`); +}); +process.on("SIGTERM", () => server.close()); diff --git a/app/server/services/user/src/routes/account.ts b/app/server/services/user/src/routes/account.ts new file mode 100644 index 0000000..c5ec990 --- /dev/null +++ b/app/server/services/user/src/routes/account.ts @@ -0,0 +1,34 @@ +// account creation route to db service + +//TODO import db connection +import { Router } from "express"; +import type { Request, Response } from "express"; +import type { ResultSetHeader } from "mysql2"; +import { db } from "../db/connection"; + +export const accountsRouter = Router(); + +accountsRouter.post("/", async (req: Request, res: Response) => { + try { + console.log(req.body) + const { name, type, balance, value, subtype } = req.body; + + const [result] = await db.query( + `INSERT INTO financialAccount (name, type, balance, value, subtype) + VALUES (?, ?, ?, ?, ?)`, + [name, type, balance, value, subtype ?? null] + ); + + res.json({ + message: "Account successfully created", + id: result.insertId, + lastUpdated: new Date() + + }); + + } catch (err) { + console.error("Account creation failed", err); + res.status(500).json({ error: "Account creation failed" }); + } +}); + diff --git a/app/server/services/user/src/routes/profile.ts b/app/server/services/user/src/routes/profile.ts new file mode 100644 index 0000000..e811bb5 --- /dev/null +++ b/app/server/services/user/src/routes/profile.ts @@ -0,0 +1,30 @@ +// profile creation route this lets us create profiles in the db , which can be used to group accounts together and track them. + +import { Router } from "express"; +import type { Request, Response } from "express"; +import type { ResultSetHeader } from "mysql2"; +import { db } from "../db/connection"; + +export const profilesRouter = Router(); + +// Create a profile +profilesRouter.post("/", async (req: Request, res: Response) => { + try { + const { name, description } = req.body; + + const [result] = await db.query( + `INSERT INTO profile (name, description) + VALUES (?, ?)`, + [name, description ?? null] + ); + + res.json({ + message: "Profile successfully created", + id: result.insertId + }); + } catch (err) { + console.error("Profile creation failed:", err); + res.status(500).json({ error: "Profile creation failed" }); + } +}); + diff --git a/app/server/services/user/src/routes/transaction.ts b/app/server/services/user/src/routes/transaction.ts new file mode 100644 index 0000000..c03fe80 --- /dev/null +++ b/app/server/services/user/src/routes/transaction.ts @@ -0,0 +1,107 @@ +// transaction routes for creating and updating transactions +import { Router } from "express"; +import type { Request, Response } from "express"; +import type { ResultSetHeader } from "mysql2"; +import { db } from "../db/connection"; + +export const transactionsRouter = Router(); + +//creating a new transaction + +transactionsRouter.post("/", async (req: Request, res: Response) => { + try { + const { financialAccount_id, amount, description, sender, recipient, date } = req.body; + + const [result] = await db.query( + `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) + VALUES (?, ?, ?, ?, ?, ?)`, + [financialAccount_id, amount, description ?? null, sender ?? null, recipient ?? null, date] + ); + + res.json({ + message: "Transaction successfully created", + id: result.insertId + }); + } catch (err) { + console.error("Transaction creation failed", err); + res.status(500).json({ error: "Transaction creation failed" }); + } +}); + + + +// Update an existing transaction +transactionsRouter.put("/:id", async (req: Request, res: Response) => { + try { + const id = req.params.id; + const {amount,description, sender, recipient, date} = req.body; + + await db.query( + `UPDATE transaction + SET amount=?, description=?, sender=?, recipient=?, date=? + WHERE id=?`, + [amount, description ?? null, sender ?? null, recipient ?? null, date, id] + ); + + res.json({ message: "Transaction successfully updated" }); + } catch (err) { + console.error("Transaction update failed", err); } +}); + +// CSV Transaction +transactionsRouter.post("/csvTransaction", async (req: Request, res: Response) => { + try { + const { financialAccount_id, transactions } = req.body; + + if (!financialAccount_id || !Array.isArray(transactions)) { + return res.status(400).json({ error: "Invalid request payload" }); + } + + const validRows = transactions.filter((t: any) => t.errors.length === 0); + + if (validRows.length === 0) { + return res.status(400).json({ error: "No valid transactions to import" }); + } + + const connection = await db.getConnection(); + + try { + await connection.beginTransaction(); + + for (const row of validRows) { + await connection.query( + `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + financialAccount_id, + row.amount, + row.description ?? null, + row.sender ?? null, + row.recipient ?? null, + row.date, + ] + ); + } + + await connection.commit(); + + res.json({ + message: "CSV transactions imported successfully", + inserted: validRows.length, + skipped: transactions.length - validRows.length, + }); + + } catch (err) { + await connection.rollback(); + console.error("CSV transaction import failed", err); + res.status(500).json({ error: "CSV transaction import failed" }); + + } finally { + connection.release(); + } + + } catch (err) { + console.error("CSV transaction import failed", err); + res.status(500).json({ error: "CSV transaction import failed" }); + } +}); From 61e012e4aa359b0f726990122621f24a622ec953 Mon Sep 17 00:00:00 2001 From: Logan Date: Tue, 3 Mar 2026 11:35:20 -0600 Subject: [PATCH 118/204] update README.md --- app/server/services/ts/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/server/services/ts/README.md b/app/server/services/ts/README.md index 50a0879..dc1e64b 100644 --- a/app/server/services/ts/README.md +++ b/app/server/services/ts/README.md @@ -8,3 +8,6 @@ Unless a package will be used in multiple modules install it only in the subdire The `common` folder is home for modules that are used by one or more services. For example `common/port.ts` is used by all services to determine which port to bind their webserver to. +### Tests +To run tests use `bun run vitest` +to see the coverage run `bun run vitest --coverage` From 5e2c97c76919a1fa5a8e29b09711468a121e5a9b Mon Sep 17 00:00:00 2001 From: Logan Date: Tue, 3 Mar 2026 11:37:29 -0600 Subject: [PATCH 119/204] commit hook bugfix --- hooks/format_ts_dir.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/format_ts_dir.sh b/hooks/format_ts_dir.sh index f9168c9..222863a 100755 --- a/hooks/format_ts_dir.sh +++ b/hooks/format_ts_dir.sh @@ -1,9 +1,10 @@ #!/bin/bash #first argument ($1) is the directory to format the files of. changed_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E $1/) -if [[ $changed_files == '' ]]; then +if [ -n $changed_files ]; then exit 0 fi +echo "files: $changed_files" normalized_files=$(echo $changed_files | sed "s|$1/||g" ) cd $1 From 3b8375f5b2d48c1b15e8acb094f5ea8973eb25ab Mon Sep 17 00:00:00 2001 From: Logan Date: Tue, 3 Mar 2026 11:54:30 -0600 Subject: [PATCH 120/204] Added new meeting minutes --- ...ting 1 - 11 Jan.md => Meeting_1_Jan-11.md} | 0 ...ting 2 - 21 Jan.md => Meeting_2_Jan-21.md} | 0 ...ting 3 - 26 Jan.md => Meeting_3_Jan-26.md} | 0 ...eeting 4 - 2 Feb.md => Meeting_4_Feb-2.md} | 0 meeting-minutes/Meeting_5_Feb-16.md | 54 +++++++++++++++++++ meeting-minutes/Meeting_6_Feb-23.md | 43 +++++++++++++++ meeting-minutes/Meeting_7_Mar-02.md | 52 ++++++++++++++++++ 7 files changed, 149 insertions(+) rename meeting-minutes/{Comp 4350 Group 6 - Meeting 1 - 11 Jan.md => Meeting_1_Jan-11.md} (100%) rename meeting-minutes/{Comp 4350 Group 6 - Meeting 2 - 21 Jan.md => Meeting_2_Jan-21.md} (100%) rename meeting-minutes/{Comp 4350 Group 6 - Meeting 3 - 26 Jan.md => Meeting_3_Jan-26.md} (100%) rename meeting-minutes/{Comp 4350 Group 6 - Meeting 4 - 2 Feb.md => Meeting_4_Feb-2.md} (100%) create mode 100644 meeting-minutes/Meeting_5_Feb-16.md create mode 100644 meeting-minutes/Meeting_6_Feb-23.md create mode 100644 meeting-minutes/Meeting_7_Mar-02.md diff --git a/meeting-minutes/Comp 4350 Group 6 - Meeting 1 - 11 Jan.md b/meeting-minutes/Meeting_1_Jan-11.md similarity index 100% rename from meeting-minutes/Comp 4350 Group 6 - Meeting 1 - 11 Jan.md rename to meeting-minutes/Meeting_1_Jan-11.md diff --git a/meeting-minutes/Comp 4350 Group 6 - Meeting 2 - 21 Jan.md b/meeting-minutes/Meeting_2_Jan-21.md similarity index 100% rename from meeting-minutes/Comp 4350 Group 6 - Meeting 2 - 21 Jan.md rename to meeting-minutes/Meeting_2_Jan-21.md diff --git a/meeting-minutes/Comp 4350 Group 6 - Meeting 3 - 26 Jan.md b/meeting-minutes/Meeting_3_Jan-26.md similarity index 100% rename from meeting-minutes/Comp 4350 Group 6 - Meeting 3 - 26 Jan.md rename to meeting-minutes/Meeting_3_Jan-26.md diff --git a/meeting-minutes/Comp 4350 Group 6 - Meeting 4 - 2 Feb.md b/meeting-minutes/Meeting_4_Feb-2.md similarity index 100% rename from meeting-minutes/Comp 4350 Group 6 - Meeting 4 - 2 Feb.md rename to meeting-minutes/Meeting_4_Feb-2.md diff --git a/meeting-minutes/Meeting_5_Feb-16.md b/meeting-minutes/Meeting_5_Feb-16.md new file mode 100644 index 0000000..c9a2999 --- /dev/null +++ b/meeting-minutes/Meeting_5_Feb-16.md @@ -0,0 +1,54 @@ +Meeting 5 agenda + +* Updates \- Record what we’ve all done so far +* Blockers +* Review what we need to get done +* Assign work and set deadlines + +Completed so far: +DB: good to go \- just need a script to seed it +Visualisation: UI is up. No logic \- Roman needs to add some graphs +User data input: Jackie done forms +User auth: good progress in integrating with develop + +Blockers: +Midterms\!\!\! + +Sprint 2 Requirements: + +**50% (3) core features** + +CI: + +* Auto Build \- Logan +* Regression Testing \- getting them to run +* Code Review + +Architecture: + +* [Sequence diagram](https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams) for each feature + + +Testing: + +* 100% back-end code coverage +* 10 unit tests per feature (integration tests not mandatory yet) +* [Test plan](https://universityofmanitoba.desire2learn.com/d2l/le/content/655209/viewContent/4633320/View) \- Roman, by 1 March + +Testing Libraries: + +* TS \- Vitest +* Python \- Pytest + +Features: + +* Visualization dashboard \- Feb 24, polish afterwards and in future sprints +* User Authentication \- Feb 24 +* User Data input \- Feb 24 + +CI Auto build \- Feb 24th + +Jobs: +User input: CSV parsing, categorizing, and updating transactions + +People who work on features write the tests \+ sequence diagrams. diff --git a/meeting-minutes/Meeting_6_Feb-23.md b/meeting-minutes/Meeting_6_Feb-23.md new file mode 100644 index 0000000..6604b22 --- /dev/null +++ b/meeting-minutes/Meeting_6_Feb-23.md @@ -0,0 +1,43 @@ +Meeting 6 agenda + +* Updates \- Record what we’ve all done so far +* Blockers +* Review what we need to get done +* Assign work and set deadlines + +Completed so far: + +Features: + +* Visualization dashboard + * Components done + * Just need to get backend to communicate with the database +* User Authentication + * Backend working + * Need to transition to shadcn +* User Data input + * Form communicating w/ server + * Server needs work + * Need page for accounts & transactions + * CSV parsing mostly done \- just needs and a success modal\! +* Auto Build + * Backend working, need to get front end dockerized +* Tests + * 0% done + +Wrap up due dates: +Roman March March 4th +Everyone else Monday March 2nd + +Jobs: +User input: CSV parsing, categorizing, and updating transactions + +Logan: +Regression Testing \- getting them to run +Code Review + +Everyone: +[Sequence diagram](https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams) for each feature +100% back-end code coverage +10 unit tests per feature (integration tests not mandatory yet) +[Test plan](https://universityofmanitoba.desire2learn.com/d2l/le/content/655209/viewContent/4633320/View) \- Roman, by 1 March \ No newline at end of file diff --git a/meeting-minutes/Meeting_7_Mar-02.md b/meeting-minutes/Meeting_7_Mar-02.md new file mode 100644 index 0000000..c1c2c6f --- /dev/null +++ b/meeting-minutes/Meeting_7_Mar-02.md @@ -0,0 +1,52 @@ +**Agenda** + +* Updates +* Cover new & catch-up work +* Assign work and set deadlines + +Features: + +* Visualization dashboard: Roman, Huy + * ~~Backend to communicate with database~~ + * 10 unit tests + * Sequence diagram +* User Authentication: Logan & Deep + * ~~Transition to shadcn~~ + * ~~10 unit tests~~ + * Sequence diagram +* User Data input: Jackie & Dylan + * Form communicates w/ server + * ~~Server side complete~~ + * ~~accounts & transactions pages~~ + * ~~CSV parsing~~ + * 10 unit tests + * Sequence diagram +* CI + * ~~Dockerize frontent~~ + * ~~Publish images automatically~~ + * ~~Run tests automatically~~ +* Test plan + * Complete it + +TODO: + +* Sprint 1 feedback + * Add a high-level architecture diagram directly to your GitHub README to give a quick technical overview. + * Aim for more detail in your documentation and task planning. + * ~~Use GitHub Issues to track your high-level user stories.~~ + * ~~Assign specific subtasks to each user story for better clarity.~~ + * Group your tasks clearly into their respective sprints. +* Wrap up + * Close complete dev tasks and features on github + * All meeting minutes on github wiki \- Logan +* Carry over + * Troubleshoot + * Visualization dashboard \- need to do calculations + * User data few bugs + +Next meeting: +Features to take next sprint +Technical seminar + +* Microservices, Docker? + From 44059059bd792ba0dfc88327ded55af6eb5b41c0 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Tue, 3 Mar 2026 14:40:35 -0600 Subject: [PATCH 121/204] Cleaned up code and moved around files so that they'll be ready for the merge into dev branch. --- app/client/src/App.tsx | 2 +- app/client/src/api/Account.ts | 4 +- app/client/src/api/Income.ts | 4 +- app/client/src/api/Transaction.ts | 4 +- .../src/components/csvread/CsvConfirm.tsx | 2 +- .../src/components/csvread/CsvPreview.tsx | 2 +- .../src/components/csvread/CsvUpload.tsx | 4 +- app/client/src/components/ui/AccountForm.tsx | 6 +- app/client/src/components/ui/AccountList.tsx | 2 +- .../src/components/ui/AccountListCard.tsx | 2 +- app/client/src/components/ui/IncomeCard.tsx | 2 +- app/client/src/components/ui/IncomeForm.tsx | 6 +- app/client/src/components/ui/IncomeList.tsx | 2 +- .../src/components/ui/TransactionCard.tsx | 2 +- .../src/components/ui/TransactionList.tsx | 2 +- .../src/components/ui/TransationForm.tsx | 9 +- app/client/src/{type => types}/AccountType.ts | 0 app/client/src/{type => types}/IncomeType.ts | 0 .../src/{type => types}/TransactionType.ts | 0 .../src/{type => types}/responseTypes.ts | 0 .../src/{util => utils}/ConvertTransaction.ts | 0 .../src/{util => utils}/NormalizeRow.ts | 0 app/client/src/{util => utils}/ParseCsv.ts | 0 .../src/{util => utils}/ValidateFile.ts | 0 .../src/{util => utils}/ValidateForms.ts | 0 app/client/src/{util => utils}/ValidateRow.ts | 0 app/client/src/{util => utils}/handleInput.ts | 0 app/server/database/schema.sql | 153 -------------- app/server/docker-compose.yml | 10 +- app/server/services/database/Dockerfile | 12 -- app/server/services/database/README.md | 15 -- app/server/services/database/bun.lock | 190 ------------------ app/server/services/database/package.json | 20 -- app/server/services/database/schema.sql | 145 ++++++++++++- app/server/services/database/src/index.ts | 17 -- .../database/src/models/FinancialAccount.ts | 11 - .../services/database/src/models/Profile.ts | 7 - .../database/src/models/ProfileLink.ts | 6 - .../database/src/models/Transaction.ts | 11 - .../services/database/src/routes/account.ts | 33 --- .../{ => services}/database/updateSchema.sh | 0 .../services/{ => ts}/analytics/Dockerfile | 0 .../{ => ts}/analytics/requirements.txt | 0 .../services/{ => ts}/analytics/src/main.py | 0 .../services/{ => ts}/api-gateway/.gitignore | 0 .../services/{ => ts}/api-gateway/Dockerfile | 0 .../services/{ => ts}/api-gateway/README.md | 0 .../services/{ => ts}/api-gateway/bun.lock | 0 .../services/{ => ts}/api-gateway/bun.lockb | Bin .../{ => ts}/api-gateway/eslint.config.mts | 0 .../{ => ts}/api-gateway/package.json | 0 .../{ => ts}/api-gateway/src/index.ts | 0 .../{ => ts}/api-gateway/tsconfig.json | 0 app/server/services/{ => ts}/auth/.gitignore | 0 app/server/services/{ => ts}/auth/Dockerfile | 0 app/server/services/{ => ts}/auth/README.md | 0 app/server/services/{ => ts}/auth/bun.lock | 0 app/server/services/{ => ts}/auth/bun.lockb | Bin .../services/{ => ts}/auth/eslint.config.ts | 0 .../services/{ => ts}/auth/package.json | 0 .../{ => ts}/auth/src/credentialUtils.ts | 0 .../services/{ => ts}/auth/src/index.ts | 0 .../services/{ => ts}/auth/tsconfig.json | 0 .../services/{ => ts}/market/Dockerfile | 0 .../services/{ => ts}/market/requirements.txt | 0 .../services/{ => ts}/market/src/main.py | 0 .../services/{database => ts/user}/.gitignore | 0 app/server/services/{ => ts}/user/Dockerfile | 0 app/server/services/{ => ts}/user/README.md | 0 app/server/services/{ => ts}/user/bun.lock | 0 app/server/services/{ => ts}/user/bun.lockb | Bin .../services/{ => ts}/user/eslint.config.ts | 0 .../services/{ => ts}/user/package.json | 0 .../user}/src/db/connection.ts | 0 .../services/{ => ts}/user/src/index.ts | 0 .../{ => ts}/user/src/routes/account.ts | 0 .../user}/src/routes/profile.ts | 0 .../user}/src/routes/transaction.ts | 0 .../{database => ts/user}/tsconfig.json | 0 app/server/services/user/.gitignore | 34 ---- app/server/services/user/src/db/connection.ts | 14 -- .../services/user/src/routes/profile.ts | 30 --- .../services/user/src/routes/transaction.ts | 107 ---------- app/server/services/user/tsconfig.json | 29 --- 84 files changed, 171 insertions(+), 728 deletions(-) rename app/client/src/{type => types}/AccountType.ts (100%) rename app/client/src/{type => types}/IncomeType.ts (100%) rename app/client/src/{type => types}/TransactionType.ts (100%) rename app/client/src/{type => types}/responseTypes.ts (100%) rename app/client/src/{util => utils}/ConvertTransaction.ts (100%) rename app/client/src/{util => utils}/NormalizeRow.ts (100%) rename app/client/src/{util => utils}/ParseCsv.ts (100%) rename app/client/src/{util => utils}/ValidateFile.ts (100%) rename app/client/src/{util => utils}/ValidateForms.ts (100%) rename app/client/src/{util => utils}/ValidateRow.ts (100%) rename app/client/src/{util => utils}/handleInput.ts (100%) delete mode 100644 app/server/database/schema.sql delete mode 100644 app/server/services/database/Dockerfile delete mode 100644 app/server/services/database/README.md delete mode 100644 app/server/services/database/bun.lock delete mode 100644 app/server/services/database/package.json delete mode 100644 app/server/services/database/src/index.ts delete mode 100644 app/server/services/database/src/models/FinancialAccount.ts delete mode 100644 app/server/services/database/src/models/Profile.ts delete mode 100644 app/server/services/database/src/models/ProfileLink.ts delete mode 100644 app/server/services/database/src/models/Transaction.ts delete mode 100644 app/server/services/database/src/routes/account.ts rename app/server/{ => services}/database/updateSchema.sh (100%) mode change 100755 => 100644 rename app/server/services/{ => ts}/analytics/Dockerfile (100%) rename app/server/services/{ => ts}/analytics/requirements.txt (100%) rename app/server/services/{ => ts}/analytics/src/main.py (100%) rename app/server/services/{ => ts}/api-gateway/.gitignore (100%) rename app/server/services/{ => ts}/api-gateway/Dockerfile (100%) rename app/server/services/{ => ts}/api-gateway/README.md (100%) rename app/server/services/{ => ts}/api-gateway/bun.lock (100%) rename app/server/services/{ => ts}/api-gateway/bun.lockb (100%) mode change 100755 => 100644 rename app/server/services/{ => ts}/api-gateway/eslint.config.mts (100%) rename app/server/services/{ => ts}/api-gateway/package.json (100%) rename app/server/services/{ => ts}/api-gateway/src/index.ts (100%) rename app/server/services/{ => ts}/api-gateway/tsconfig.json (100%) rename app/server/services/{ => ts}/auth/.gitignore (100%) rename app/server/services/{ => ts}/auth/Dockerfile (100%) rename app/server/services/{ => ts}/auth/README.md (100%) rename app/server/services/{ => ts}/auth/bun.lock (100%) rename app/server/services/{ => ts}/auth/bun.lockb (100%) mode change 100755 => 100644 rename app/server/services/{ => ts}/auth/eslint.config.ts (100%) rename app/server/services/{ => ts}/auth/package.json (100%) rename app/server/services/{ => ts}/auth/src/credentialUtils.ts (100%) rename app/server/services/{ => ts}/auth/src/index.ts (100%) rename app/server/services/{ => ts}/auth/tsconfig.json (100%) rename app/server/services/{ => ts}/market/Dockerfile (100%) rename app/server/services/{ => ts}/market/requirements.txt (100%) rename app/server/services/{ => ts}/market/src/main.py (100%) rename app/server/services/{database => ts/user}/.gitignore (100%) rename app/server/services/{ => ts}/user/Dockerfile (100%) rename app/server/services/{ => ts}/user/README.md (100%) rename app/server/services/{ => ts}/user/bun.lock (100%) rename app/server/services/{ => ts}/user/bun.lockb (100%) mode change 100755 => 100644 rename app/server/services/{ => ts}/user/eslint.config.ts (100%) rename app/server/services/{ => ts}/user/package.json (100%) rename app/server/services/{database => ts/user}/src/db/connection.ts (100%) rename app/server/services/{ => ts}/user/src/index.ts (100%) rename app/server/services/{ => ts}/user/src/routes/account.ts (100%) rename app/server/services/{database => ts/user}/src/routes/profile.ts (100%) rename app/server/services/{database => ts/user}/src/routes/transaction.ts (100%) rename app/server/services/{database => ts/user}/tsconfig.json (100%) delete mode 100644 app/server/services/user/.gitignore delete mode 100644 app/server/services/user/src/db/connection.ts delete mode 100644 app/server/services/user/src/routes/profile.ts delete mode 100644 app/server/services/user/src/routes/transaction.ts delete mode 100644 app/server/services/user/tsconfig.json diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index b549985..b6354aa 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -9,7 +9,7 @@ import LoadingSpinner from './components/ui/LoadingSpinner.tsx' import AccountPopup from './components/ui/AccountForm.tsx' import TransactionPopup from './components/ui/TransationForm.tsx' import IncomePopup from './components/ui/IncomeForm.tsx' -import type { Account } from './type/AccountType.ts' +import type { Account } from './types/AccountType.ts' function App() { return ( diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index 67ce2d7..976486a 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,5 +1,5 @@ -import type { updateResponse } from "../type/responseTypes"; -import type { Account } from "../type/AccountType"; +import type { updateResponse } from "../types/responseTypes"; +import type { Account } from "../types/AccountType"; const requestUrl = 'http://localhost:3000/api/accounts' diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts index bb86b48..f458294 100644 --- a/app/client/src/api/Income.ts +++ b/app/client/src/api/Income.ts @@ -1,6 +1,6 @@ -import type { updateResponse } from "../type/responseTypes" -import { type Income } from "../type/IncomeType" +import type { updateResponse } from "../types/responseTypes" +import { type Income } from "../types/IncomeType" const requestUrl = "http://localhost:3000/api/income" diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index eaacd4e..291e8a9 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,5 +1,5 @@ -import type { updateResponse } from "../type/responseTypes"; -import { type Transaction } from "../type/TransactionType"; +import type { updateResponse } from "../types/responseTypes"; +import { type Transaction } from "../types/TransactionType"; const requestUrl = "http://localhost:3000/api/transacitons" diff --git a/app/client/src/components/csvread/CsvConfirm.tsx b/app/client/src/components/csvread/CsvConfirm.tsx index fc158f8..26dda34 100644 --- a/app/client/src/components/csvread/CsvConfirm.tsx +++ b/app/client/src/components/csvread/CsvConfirm.tsx @@ -1,6 +1,6 @@ //shows a summary of parsed CSV data before final import -import type { TransactionDraft } from "../../util/ConvertTransaction"; +import type { TransactionDraft } from "../../utils/ConvertTransaction"; interface Props { rows: TransactionDraft[]; diff --git a/app/client/src/components/csvread/CsvPreview.tsx b/app/client/src/components/csvread/CsvPreview.tsx index fba2c75..3475107 100644 --- a/app/client/src/components/csvread/CsvPreview.tsx +++ b/app/client/src/components/csvread/CsvPreview.tsx @@ -1,6 +1,6 @@ // Component to display parsed CSV rows in a preview table -import type { TransactionDraft } from "../../util/ConvertTransaction"; +import type { TransactionDraft } from "../../utils/ConvertTransaction"; interface Props { rows: TransactionDraft[]; diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index 3fa96d6..15d228a 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -6,8 +6,8 @@ import CsvFile from "./CsvFile"; import CsvError from "./CsvError"; import CsvPreviewTable from "./CsvPreview"; import CsvConfirmation from "./CsvConfirm"; -import { parseCsvFile } from "../../util/ParseCsv"; -import type { TransactionDraft } from "../../util/ConvertTransaction"; +import { parseCsvFile } from "../../utils/ParseCsv"; +import type { TransactionDraft } from "../../utils/ConvertTransaction"; import SuccessScreen from "./csvSuccess"; export default function CsvUpload({ accountId }: { accountId: number }) { diff --git a/app/client/src/components/ui/AccountForm.tsx b/app/client/src/components/ui/AccountForm.tsx index e8e9d68..0ce5d02 100644 --- a/app/client/src/components/ui/AccountForm.tsx +++ b/app/client/src/components/ui/AccountForm.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import './UserForm.css' import {postUserAccount, putUserAccount} from '../../api/Account.ts' -import { type Account } from "../../type/AccountType.ts"; -import { validateAccountForm } from "../../util/ValidateForms.ts"; -import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput.ts"; +import { type Account } from "../../types/AccountType.ts"; +import { validateAccountForm } from "../../utils/ValidateForms.ts"; +import { handleCurrencyChange, handleCurrencyBlur } from "../../utils/handleInput.ts"; interface popupProp{ diff --git a/app/client/src/components/ui/AccountList.tsx b/app/client/src/components/ui/AccountList.tsx index 9bab281..3a19e7b 100644 --- a/app/client/src/components/ui/AccountList.tsx +++ b/app/client/src/components/ui/AccountList.tsx @@ -1,6 +1,6 @@ import { useState } from "react" import { getUserAccounts} from "../../api/Account" -import { type Account } from "../../type/AccountType" +import { type Account } from "../../types/AccountType" import AccountListCard from "./AccountListCard" import AccountPopup from "./AccountForm" diff --git a/app/client/src/components/ui/AccountListCard.tsx b/app/client/src/components/ui/AccountListCard.tsx index 16add3d..87165b0 100644 --- a/app/client/src/components/ui/AccountListCard.tsx +++ b/app/client/src/components/ui/AccountListCard.tsx @@ -1,7 +1,7 @@ import { deleteUserAccount } from "../../api/Account.ts"; import { useState } from "react"; import AccountPopup from './AccountForm.tsx' -import { type Account } from "../../type/AccountType.ts"; +import { type Account } from "../../types/AccountType.ts"; interface cardProp{ account:Account diff --git a/app/client/src/components/ui/IncomeCard.tsx b/app/client/src/components/ui/IncomeCard.tsx index 4d56258..c312a3f 100644 --- a/app/client/src/components/ui/IncomeCard.tsx +++ b/app/client/src/components/ui/IncomeCard.tsx @@ -1,6 +1,6 @@ import { useState } from "react" import { deleteIncome} from "../../api/Income" -import { type Income } from "../../type/IncomeType" +import { type Income } from "../../types/IncomeType" import IncomePopup from "./IncomeForm" interface cardProp{ diff --git a/app/client/src/components/ui/IncomeForm.tsx b/app/client/src/components/ui/IncomeForm.tsx index 5b9defc..bd30a56 100644 --- a/app/client/src/components/ui/IncomeForm.tsx +++ b/app/client/src/components/ui/IncomeForm.tsx @@ -1,9 +1,9 @@ import { useState } from "react" -import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput" +import { handleCurrencyChange, handleCurrencyBlur } from "../../utils/handleInput" import './UserForm.css' import {postIncome, putIncome} from "../../api/Income" -import { validateIncomeForm } from "../../util/ValidateForms"; -import { type Income } from "../../type/IncomeType"; +import { validateIncomeForm } from "../../utils/ValidateForms"; +import { type Income } from "../../types/IncomeType"; interface popupProp{ toggle: () => void; diff --git a/app/client/src/components/ui/IncomeList.tsx b/app/client/src/components/ui/IncomeList.tsx index 7bea7a7..889e38a 100644 --- a/app/client/src/components/ui/IncomeList.tsx +++ b/app/client/src/components/ui/IncomeList.tsx @@ -1,6 +1,6 @@ import { useState } from "react" import { getIncome} from "../../api/Income" -import { type Income } from "../../type/IncomeType" +import { type Income } from "../../types/IncomeType" import IncomePopup from "./IncomeForm" import IncomeCard from "./IncomeCard" diff --git a/app/client/src/components/ui/TransactionCard.tsx b/app/client/src/components/ui/TransactionCard.tsx index f527ad9..69ca565 100644 --- a/app/client/src/components/ui/TransactionCard.tsx +++ b/app/client/src/components/ui/TransactionCard.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { deleteTransaction} from "../../api/Transaction"; -import { type Transaction } from "../../type/TransactionType"; +import { type Transaction } from "../../types/TransactionType"; import TransactionPopup from "./TransationForm" diff --git a/app/client/src/components/ui/TransactionList.tsx b/app/client/src/components/ui/TransactionList.tsx index a786e2d..d7f8335 100644 --- a/app/client/src/components/ui/TransactionList.tsx +++ b/app/client/src/components/ui/TransactionList.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { getTransactions} from "../../api/Transaction"; -import { type Transaction } from "../../type/TransactionType"; +import { type Transaction } from "../../types/TransactionType"; import TransactionPopup from "./TransationForm" import TransactionCard from "./TransactionCard" diff --git a/app/client/src/components/ui/TransationForm.tsx b/app/client/src/components/ui/TransationForm.tsx index 52524cd..f5b100f 100644 --- a/app/client/src/components/ui/TransationForm.tsx +++ b/app/client/src/components/ui/TransationForm.tsx @@ -2,11 +2,11 @@ import React, { useState} from "react"; import CsvUpload from "../csvread/CsvUpload"; import './UserForm.css' import { getUserAccounts} from "../../api/Account"; -import { validateTransactionForm } from "../../util/ValidateForms"; +import { validateTransactionForm } from "../../utils/ValidateForms"; import { postTranscations, putTranscations} from "../../api/Transaction"; -import { handleCurrencyChange, handleCurrencyBlur } from "../../util/handleInput"; -import { type Transaction } from "../../type/TransactionType"; -import { type Account } from "../../type/AccountType"; +import { handleCurrencyChange, handleCurrencyBlur } from "../../utils/handleInput"; +import { type Transaction } from "../../types/TransactionType"; +import { type Account } from "../../types/AccountType"; interface popupProp{ toggle: () => void; @@ -22,6 +22,7 @@ export const transactionCategory = { HOUSING: "Housing", UTIL: "Utilzities", TRANSPORTATION: "Transportation", + INCOME:"INCOME", OTHER: "Other", }; diff --git a/app/client/src/type/AccountType.ts b/app/client/src/types/AccountType.ts similarity index 100% rename from app/client/src/type/AccountType.ts rename to app/client/src/types/AccountType.ts diff --git a/app/client/src/type/IncomeType.ts b/app/client/src/types/IncomeType.ts similarity index 100% rename from app/client/src/type/IncomeType.ts rename to app/client/src/types/IncomeType.ts diff --git a/app/client/src/type/TransactionType.ts b/app/client/src/types/TransactionType.ts similarity index 100% rename from app/client/src/type/TransactionType.ts rename to app/client/src/types/TransactionType.ts diff --git a/app/client/src/type/responseTypes.ts b/app/client/src/types/responseTypes.ts similarity index 100% rename from app/client/src/type/responseTypes.ts rename to app/client/src/types/responseTypes.ts diff --git a/app/client/src/util/ConvertTransaction.ts b/app/client/src/utils/ConvertTransaction.ts similarity index 100% rename from app/client/src/util/ConvertTransaction.ts rename to app/client/src/utils/ConvertTransaction.ts diff --git a/app/client/src/util/NormalizeRow.ts b/app/client/src/utils/NormalizeRow.ts similarity index 100% rename from app/client/src/util/NormalizeRow.ts rename to app/client/src/utils/NormalizeRow.ts diff --git a/app/client/src/util/ParseCsv.ts b/app/client/src/utils/ParseCsv.ts similarity index 100% rename from app/client/src/util/ParseCsv.ts rename to app/client/src/utils/ParseCsv.ts diff --git a/app/client/src/util/ValidateFile.ts b/app/client/src/utils/ValidateFile.ts similarity index 100% rename from app/client/src/util/ValidateFile.ts rename to app/client/src/utils/ValidateFile.ts diff --git a/app/client/src/util/ValidateForms.ts b/app/client/src/utils/ValidateForms.ts similarity index 100% rename from app/client/src/util/ValidateForms.ts rename to app/client/src/utils/ValidateForms.ts diff --git a/app/client/src/util/ValidateRow.ts b/app/client/src/utils/ValidateRow.ts similarity index 100% rename from app/client/src/util/ValidateRow.ts rename to app/client/src/utils/ValidateRow.ts diff --git a/app/client/src/util/handleInput.ts b/app/client/src/utils/handleInput.ts similarity index 100% rename from app/client/src/util/handleInput.ts rename to app/client/src/utils/handleInput.ts diff --git a/app/server/database/schema.sql b/app/server/database/schema.sql deleted file mode 100644 index d4d58d1..0000000 --- a/app/server/database/schema.sql +++ /dev/null @@ -1,153 +0,0 @@ -CREATE DATABASE finus; - -CREATE TABLE finus.finusAccount ( - id INTEGER NOT NULL AUTO_INCREMENT, - username VARCHAR(100) NOT NULL, - email VARCHAR(100) NOT NULL, - first_name VARCHAR(100) NOT NULL, - last_name VARCHAR(100) NOT NULL, - age INTEGER NOT NULL, - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id) -); - -CREATE TABLE finus.credentials ( - finus_account_id INTEGER NOT NULL AUTO_INCREMENT, - pw_hash BLOB(256) NOT NULL, - salt CHAR(8) NOT NULL, - PRIMARY KEY (finus_account_id), - FOREIGN KEY (finus_account_id) REFERENCES finus.finusAccount(id) ON DELETE CASCADE -); - -CREATE TABLE finus.profile ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(100) NOT NULL, - description VARCHAR(500), - PRIMARY KEY (id) -); - -CREATE TABLE finus.goalType ( - type VARCHAR(50) NOT NULL, - PRIMARY KEY (type) -); - -CREATE TABLE finus.goal ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL, - type VARCHAR(50) NOT NULL, - objective VARCHAR(500) NOT NULL, - description VARCHAR(500), - deadline DATETIME, - PRIMARY KEY (id), - FOREIGN KEY (type) REFERENCES finus.goalType(type) ON DELETE CASCADE - -); - -CREATE TABLE finus.profile_goal ( - profile_id INTEGER NOT NULL, - goal_id INTEGER NOT NULL, - PRIMARY KEY (profile_id, goal_id), - FOREIGN KEY (profile_id) REFERENCES finus.profile(id), - FOREIGN KEY (goal_id) REFERENCES finus.goal(id) -); - -CREATE TABLE finus.financialAccountType ( - type VARCHAR(50) NOT NULL, - PRIMARY KEY (type) -); - -CREATE TABLE finus.financialAccountSubtype( - type VARCHAR(50) NOT NULL, - PRIMARY KEY (type) -); - -CREATE TABLE finus.financialAccount ( - id INTEGER NOT NULL, - name VARCHAR(50) NOT NULL, - type VARCHAR(50) NOT NULL, - balance INTEGER NOT NULL, - value INTEGER NOT NULL, - last_updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - subtype VARCHAR(50), - PRIMARY KEY (id), - FOREIGN KEY (type) REFERENCES finus.financialAccountType(type) ON DELETE CASCADE, - FOREIGN KEY (subtype) REFERENCES finus.financialAccountType(type) ON DELETE CASCADE - -); - -CREATE TABLE finus.finusAccount_profile ( - profile_id INTEGER NOT NULL, - account_id INTEGER NOT NULL, - PRIMARY KEY (profile_id, account_id), - FOREIGN KEY (profile_id) REFERENCES finus.profile(id), - FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE -); - -CREATE TABLE finus.transaction ( - id INTEGER NOT NULL AUTO_INCREMENT, - financialAccount_id INTEGER NOT NULL, - amount INTEGER NOT NULL, - description VARCHAR(500), - sender VARCHAR(50), - recipient VARCHAR(50), - date DATETIME, - PRIMARY KEY (id), - FOREIGN KEY (financialAccount_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE -); - -CREATE TABLE finus.asset ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL, - fixed_compound_rate DOUBLE, - PRIMARY KEY (id) -); - -CREATE TABLE finus.investmentType( - type VARCHAR(50) NOT NULL, - PRIMARY KEY (type) -); - -CREATE TABLE finus.investment ( - id INTEGER NOT NULL AUTO_INCREMENT, - account_id INTEGER NOT NULL, - type VARCHAR(50) NOT NULL, - name VARCHAR(50) NOT NULL, - description VARCHAR(500) NOT NULL, - PRIMARY KEY (id), - FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE, - FOREIGN KEY (type) REFERENCES finus.investmentType(type) -); - - -CREATE TABLE finus.investmentState ( - investment_id INTEGER NOT NULL, - quantity DOUBLE NOT NULL DEFAULT 1, - total_cost DOUBLE NOT NULL, - at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (investment_id), - FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE -); - - -## Example of adding extra data -CREATE TABLE finus.stockInvestment( - investment_id INTEGER NOT NULL, - symbol VARCHAR(50) NOT NULL, - PRIMARY KEY (investment_id), - FOREIGN KEY (investment_id) REFERENCES finus.investment(id) -); - -CREATE TABLE finus.fixedInterestInvestment( - investment_id INTEGER NOT NULL, - rate DOUBLE NOT NULL, - frequency_years INTEGER NOT NULL, - frequency_months INTEGER NOT NULL, - PRIMARY KEY (investment_id), - FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE -); - -#Populate lookup tables -#INSERT INTO finus.financialAccountType (type) VALUES (); -#INSERT INTO finus.financialAccountSubtype (type) VALUES (); -INSERT INTO finus.investmentType (type) VALUES ('fixedInterest'), ('stock'); #These have to match table names -INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 5b1aa57..756f050 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -1,7 +1,7 @@ services: api-gateway: - build: ./services/api-gateway + build: ./services/ts/api-gateway ports: - "3000:8000" env_file: @@ -15,22 +15,22 @@ services: - market auth: - build: ./services/auth + build: ./services/ts/auth env_file: - path: ./default.env user: - build: ./services/user + build: ./services/ts/user env_file: - path: ./default.env analytics: - build: ./services/analytics + build: ./services/ts/analytics env_file: - path: ./default.env market: - build: ./services/market + build: ./services/ts/market env_file: - path: ./default.env diff --git a/app/server/services/database/Dockerfile b/app/server/services/database/Dockerfile deleted file mode 100644 index 0807b34..0000000 --- a/app/server/services/database/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM oven/bun:1.0-slim - -WORKDIR /app - -COPY package.json bun.lock ./ -RUN bun install - -COPY . . - -EXPOSE 3000 - -CMD ["bun", "run", "src/index.ts"] diff --git a/app/server/services/database/README.md b/app/server/services/database/README.md deleted file mode 100644 index 1d3c175..0000000 --- a/app/server/services/database/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# database-service - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/services/database/bun.lock b/app/server/services/database/bun.lock deleted file mode 100644 index afb1868..0000000 --- a/app/server/services/database/bun.lock +++ /dev/null @@ -1,190 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "database-service", - "dependencies": { - "express": "^4.18.2", - "mysql2": "^3.6.0", - }, - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], - - "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], - - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], - - "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - - "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], - - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - - "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], - - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], - - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - - "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], - - "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "mysql2": ["mysql2@3.17.1", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.3", "named-placeholders": "^1.1.6", "seq-queue": "^0.0.5", "sql-escaper": "^1.3.2" } }, "sha512-UzIzdVwPXPoZm+FaJ4lNsRt28HtUwt68gpLH7NP1oSjd91M5Qn1XJzbIsSRMRc5CV3pvktLNshmbaFfMYqPBhQ=="], - - "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], - - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], - - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "sql-escaper": ["sql-escaper@1.3.2", "", {}, "sha512-lp+ZDVfSjHt+qAK1jXBTIXBNYnbo7gnaAGwoYTH9bE89kNkXwcu6g0WjJGRsdTKVpY1z70u3Y0IgmnBOoRybHw=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - } -} diff --git a/app/server/services/database/package.json b/app/server/services/database/package.json deleted file mode 100644 index 91e07b7..0000000 --- a/app/server/services/database/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "database-service", - "version": "1.0.0", - "type": "module", - "scripts": { - "start": "bun run src/index.ts" - }, - "dependencies": { - "express": "^4.18.2", - "mysql2": "^3.6.0" - }, - "module": "index.ts", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } -} diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql index affa45b..d4d58d1 100644 --- a/app/server/services/database/schema.sql +++ b/app/server/services/database/schema.sql @@ -1,7 +1,7 @@ CREATE DATABASE finus; -CREATE TABLE finus.account ( - id INTEGER NOT NULL AUTO_INCREMENT, +CREATE TABLE finus.finusAccount ( + id INTEGER NOT NULL AUTO_INCREMENT, username VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL, @@ -12,11 +12,142 @@ CREATE TABLE finus.account ( ); CREATE TABLE finus.credentials ( - account_id INTEGER NOT NULL, - pw_hash BLOB(256) NOT NULL, - salt CHAR(8) NOT NULL, - PRIMARY KEY (account_id), - FOREIGN KEY (account_id) REFERENCES finus.account(id) ON DELETE CASCADE + finus_account_id INTEGER NOT NULL AUTO_INCREMENT, + pw_hash BLOB(256) NOT NULL, + salt CHAR(8) NOT NULL, + PRIMARY KEY (finus_account_id), + FOREIGN KEY (finus_account_id) REFERENCES finus.finusAccount(id) ON DELETE CASCADE ); +CREATE TABLE finus.profile ( + id INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + PRIMARY KEY (id) +); + +CREATE TABLE finus.goalType ( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + +CREATE TABLE finus.goal ( + id INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + type VARCHAR(50) NOT NULL, + objective VARCHAR(500) NOT NULL, + description VARCHAR(500), + deadline DATETIME, + PRIMARY KEY (id), + FOREIGN KEY (type) REFERENCES finus.goalType(type) ON DELETE CASCADE + +); + +CREATE TABLE finus.profile_goal ( + profile_id INTEGER NOT NULL, + goal_id INTEGER NOT NULL, + PRIMARY KEY (profile_id, goal_id), + FOREIGN KEY (profile_id) REFERENCES finus.profile(id), + FOREIGN KEY (goal_id) REFERENCES finus.goal(id) +); + +CREATE TABLE finus.financialAccountType ( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + +CREATE TABLE finus.financialAccountSubtype( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + +CREATE TABLE finus.financialAccount ( + id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + type VARCHAR(50) NOT NULL, + balance INTEGER NOT NULL, + value INTEGER NOT NULL, + last_updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + subtype VARCHAR(50), + PRIMARY KEY (id), + FOREIGN KEY (type) REFERENCES finus.financialAccountType(type) ON DELETE CASCADE, + FOREIGN KEY (subtype) REFERENCES finus.financialAccountType(type) ON DELETE CASCADE + +); + +CREATE TABLE finus.finusAccount_profile ( + profile_id INTEGER NOT NULL, + account_id INTEGER NOT NULL, + PRIMARY KEY (profile_id, account_id), + FOREIGN KEY (profile_id) REFERENCES finus.profile(id), + FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE +); + +CREATE TABLE finus.transaction ( + id INTEGER NOT NULL AUTO_INCREMENT, + financialAccount_id INTEGER NOT NULL, + amount INTEGER NOT NULL, + description VARCHAR(500), + sender VARCHAR(50), + recipient VARCHAR(50), + date DATETIME, + PRIMARY KEY (id), + FOREIGN KEY (financialAccount_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE +); + +CREATE TABLE finus.asset ( + id INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + fixed_compound_rate DOUBLE, + PRIMARY KEY (id) +); + +CREATE TABLE finus.investmentType( + type VARCHAR(50) NOT NULL, + PRIMARY KEY (type) +); + +CREATE TABLE finus.investment ( + id INTEGER NOT NULL AUTO_INCREMENT, + account_id INTEGER NOT NULL, + type VARCHAR(50) NOT NULL, + name VARCHAR(50) NOT NULL, + description VARCHAR(500) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE, + FOREIGN KEY (type) REFERENCES finus.investmentType(type) +); + + +CREATE TABLE finus.investmentState ( + investment_id INTEGER NOT NULL, + quantity DOUBLE NOT NULL DEFAULT 1, + total_cost DOUBLE NOT NULL, + at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (investment_id), + FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE +); + + +## Example of adding extra data +CREATE TABLE finus.stockInvestment( + investment_id INTEGER NOT NULL, + symbol VARCHAR(50) NOT NULL, + PRIMARY KEY (investment_id), + FOREIGN KEY (investment_id) REFERENCES finus.investment(id) +); + +CREATE TABLE finus.fixedInterestInvestment( + investment_id INTEGER NOT NULL, + rate DOUBLE NOT NULL, + frequency_years INTEGER NOT NULL, + frequency_months INTEGER NOT NULL, + PRIMARY KEY (investment_id), + FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE +); +#Populate lookup tables +#INSERT INTO finus.financialAccountType (type) VALUES (); +#INSERT INTO finus.financialAccountSubtype (type) VALUES (); +INSERT INTO finus.investmentType (type) VALUES ('fixedInterest'), ('stock'); #These have to match table names +INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); diff --git a/app/server/services/database/src/index.ts b/app/server/services/database/src/index.ts deleted file mode 100644 index ab8dacb..0000000 --- a/app/server/services/database/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -//Index file for database service, sets up express server and routes - -import express from "express"; -import { accountsRouter } from "./routes/account"; -import { transactionsRouter } from "./routes/transaction"; -import { profilesRouter } from "./routes/profile"; - -const app = express(); -app.use(express.json()); - -app.use("/accounts", accountsRouter); -app.use("/transactions", transactionsRouter); -app.use("/profiles", profilesRouter); - -app.listen(3000, () => { - console.log("Database service running on port 3000"); -}); diff --git a/app/server/services/database/src/models/FinancialAccount.ts b/app/server/services/database/src/models/FinancialAccount.ts deleted file mode 100644 index 678cb2c..0000000 --- a/app/server/services/database/src/models/FinancialAccount.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { RowDataPacket } from "mysql2"; -// shows account types (chequing, savings, or investment accounts) -export interface FinancialAccount extends RowDataPacket { - id: number; - name: string; - type: string; - balance: number; - value: number; - subtype?: string; - last_updated: Date; -} diff --git a/app/server/services/database/src/models/Profile.ts b/app/server/services/database/src/models/Profile.ts deleted file mode 100644 index 5a653c3..0000000 --- a/app/server/services/database/src/models/Profile.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { RowDataPacket } from "mysql2"; -// users view of accounts and goals -export interface Profile extends RowDataPacket { - id: number; - name: string; - description?: string; -} diff --git a/app/server/services/database/src/models/ProfileLink.ts b/app/server/services/database/src/models/ProfileLink.ts deleted file mode 100644 index 279a6b5..0000000 --- a/app/server/services/database/src/models/ProfileLink.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { RowDataPacket } from "mysql2"; -//shows relationship betwen user and their profiles/accounts -export interface UserProfileLink extends RowDataPacket{ - profile_id: number; - account_id: number; -} diff --git a/app/server/services/database/src/models/Transaction.ts b/app/server/services/database/src/models/Transaction.ts deleted file mode 100644 index 8b5838b..0000000 --- a/app/server/services/database/src/models/Transaction.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { RowDataPacket } from "mysql2"; -//represents individual transaction events (spending, income, transfers) -export interface Transaction extends RowDataPacket{ - id: number; - financialAccount_id: number; - amount: number; - description?: string; - sender?: string; - recipient?: string; - date: Date; -} diff --git a/app/server/services/database/src/routes/account.ts b/app/server/services/database/src/routes/account.ts deleted file mode 100644 index e6a32ce..0000000 --- a/app/server/services/database/src/routes/account.ts +++ /dev/null @@ -1,33 +0,0 @@ -// account creation route to db service - -//TODO import db connection -import { Router } from "express"; -import type { Request, Response } from "express"; -import type { ResultSetHeader } from "mysql2"; -import { db } from "../db/connection"; - -export const accountsRouter = Router(); - -accountsRouter.post("/", async (req: Request, res: Response) => { - try { - const { name, type, balance, value, subtype } = req.body; - - const [result] = await db.query( - `INSERT INTO financialAccount (name, type, balance, value, subtype) - VALUES (?, ?, ?, ?, ?)`, - [name, type, balance, value, subtype ?? null] - ); - - res.json({ - message: "Account successfully created", - id: result.insertId, - lastUpdated: new Date() - - }); - - } catch (err) { - console.error("Account creation failed", err); - res.status(500).json({ error: "Account creation failed" }); - } -}); - diff --git a/app/server/database/updateSchema.sh b/app/server/services/database/updateSchema.sh old mode 100755 new mode 100644 similarity index 100% rename from app/server/database/updateSchema.sh rename to app/server/services/database/updateSchema.sh diff --git a/app/server/services/analytics/Dockerfile b/app/server/services/ts/analytics/Dockerfile similarity index 100% rename from app/server/services/analytics/Dockerfile rename to app/server/services/ts/analytics/Dockerfile diff --git a/app/server/services/analytics/requirements.txt b/app/server/services/ts/analytics/requirements.txt similarity index 100% rename from app/server/services/analytics/requirements.txt rename to app/server/services/ts/analytics/requirements.txt diff --git a/app/server/services/analytics/src/main.py b/app/server/services/ts/analytics/src/main.py similarity index 100% rename from app/server/services/analytics/src/main.py rename to app/server/services/ts/analytics/src/main.py diff --git a/app/server/services/api-gateway/.gitignore b/app/server/services/ts/api-gateway/.gitignore similarity index 100% rename from app/server/services/api-gateway/.gitignore rename to app/server/services/ts/api-gateway/.gitignore diff --git a/app/server/services/api-gateway/Dockerfile b/app/server/services/ts/api-gateway/Dockerfile similarity index 100% rename from app/server/services/api-gateway/Dockerfile rename to app/server/services/ts/api-gateway/Dockerfile diff --git a/app/server/services/api-gateway/README.md b/app/server/services/ts/api-gateway/README.md similarity index 100% rename from app/server/services/api-gateway/README.md rename to app/server/services/ts/api-gateway/README.md diff --git a/app/server/services/api-gateway/bun.lock b/app/server/services/ts/api-gateway/bun.lock similarity index 100% rename from app/server/services/api-gateway/bun.lock rename to app/server/services/ts/api-gateway/bun.lock diff --git a/app/server/services/api-gateway/bun.lockb b/app/server/services/ts/api-gateway/bun.lockb old mode 100755 new mode 100644 similarity index 100% rename from app/server/services/api-gateway/bun.lockb rename to app/server/services/ts/api-gateway/bun.lockb diff --git a/app/server/services/api-gateway/eslint.config.mts b/app/server/services/ts/api-gateway/eslint.config.mts similarity index 100% rename from app/server/services/api-gateway/eslint.config.mts rename to app/server/services/ts/api-gateway/eslint.config.mts diff --git a/app/server/services/api-gateway/package.json b/app/server/services/ts/api-gateway/package.json similarity index 100% rename from app/server/services/api-gateway/package.json rename to app/server/services/ts/api-gateway/package.json diff --git a/app/server/services/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts similarity index 100% rename from app/server/services/api-gateway/src/index.ts rename to app/server/services/ts/api-gateway/src/index.ts diff --git a/app/server/services/api-gateway/tsconfig.json b/app/server/services/ts/api-gateway/tsconfig.json similarity index 100% rename from app/server/services/api-gateway/tsconfig.json rename to app/server/services/ts/api-gateway/tsconfig.json diff --git a/app/server/services/auth/.gitignore b/app/server/services/ts/auth/.gitignore similarity index 100% rename from app/server/services/auth/.gitignore rename to app/server/services/ts/auth/.gitignore diff --git a/app/server/services/auth/Dockerfile b/app/server/services/ts/auth/Dockerfile similarity index 100% rename from app/server/services/auth/Dockerfile rename to app/server/services/ts/auth/Dockerfile diff --git a/app/server/services/auth/README.md b/app/server/services/ts/auth/README.md similarity index 100% rename from app/server/services/auth/README.md rename to app/server/services/ts/auth/README.md diff --git a/app/server/services/auth/bun.lock b/app/server/services/ts/auth/bun.lock similarity index 100% rename from app/server/services/auth/bun.lock rename to app/server/services/ts/auth/bun.lock diff --git a/app/server/services/auth/bun.lockb b/app/server/services/ts/auth/bun.lockb old mode 100755 new mode 100644 similarity index 100% rename from app/server/services/auth/bun.lockb rename to app/server/services/ts/auth/bun.lockb diff --git a/app/server/services/auth/eslint.config.ts b/app/server/services/ts/auth/eslint.config.ts similarity index 100% rename from app/server/services/auth/eslint.config.ts rename to app/server/services/ts/auth/eslint.config.ts diff --git a/app/server/services/auth/package.json b/app/server/services/ts/auth/package.json similarity index 100% rename from app/server/services/auth/package.json rename to app/server/services/ts/auth/package.json diff --git a/app/server/services/auth/src/credentialUtils.ts b/app/server/services/ts/auth/src/credentialUtils.ts similarity index 100% rename from app/server/services/auth/src/credentialUtils.ts rename to app/server/services/ts/auth/src/credentialUtils.ts diff --git a/app/server/services/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts similarity index 100% rename from app/server/services/auth/src/index.ts rename to app/server/services/ts/auth/src/index.ts diff --git a/app/server/services/auth/tsconfig.json b/app/server/services/ts/auth/tsconfig.json similarity index 100% rename from app/server/services/auth/tsconfig.json rename to app/server/services/ts/auth/tsconfig.json diff --git a/app/server/services/market/Dockerfile b/app/server/services/ts/market/Dockerfile similarity index 100% rename from app/server/services/market/Dockerfile rename to app/server/services/ts/market/Dockerfile diff --git a/app/server/services/market/requirements.txt b/app/server/services/ts/market/requirements.txt similarity index 100% rename from app/server/services/market/requirements.txt rename to app/server/services/ts/market/requirements.txt diff --git a/app/server/services/market/src/main.py b/app/server/services/ts/market/src/main.py similarity index 100% rename from app/server/services/market/src/main.py rename to app/server/services/ts/market/src/main.py diff --git a/app/server/services/database/.gitignore b/app/server/services/ts/user/.gitignore similarity index 100% rename from app/server/services/database/.gitignore rename to app/server/services/ts/user/.gitignore diff --git a/app/server/services/user/Dockerfile b/app/server/services/ts/user/Dockerfile similarity index 100% rename from app/server/services/user/Dockerfile rename to app/server/services/ts/user/Dockerfile diff --git a/app/server/services/user/README.md b/app/server/services/ts/user/README.md similarity index 100% rename from app/server/services/user/README.md rename to app/server/services/ts/user/README.md diff --git a/app/server/services/user/bun.lock b/app/server/services/ts/user/bun.lock similarity index 100% rename from app/server/services/user/bun.lock rename to app/server/services/ts/user/bun.lock diff --git a/app/server/services/user/bun.lockb b/app/server/services/ts/user/bun.lockb old mode 100755 new mode 100644 similarity index 100% rename from app/server/services/user/bun.lockb rename to app/server/services/ts/user/bun.lockb diff --git a/app/server/services/user/eslint.config.ts b/app/server/services/ts/user/eslint.config.ts similarity index 100% rename from app/server/services/user/eslint.config.ts rename to app/server/services/ts/user/eslint.config.ts diff --git a/app/server/services/user/package.json b/app/server/services/ts/user/package.json similarity index 100% rename from app/server/services/user/package.json rename to app/server/services/ts/user/package.json diff --git a/app/server/services/database/src/db/connection.ts b/app/server/services/ts/user/src/db/connection.ts similarity index 100% rename from app/server/services/database/src/db/connection.ts rename to app/server/services/ts/user/src/db/connection.ts diff --git a/app/server/services/user/src/index.ts b/app/server/services/ts/user/src/index.ts similarity index 100% rename from app/server/services/user/src/index.ts rename to app/server/services/ts/user/src/index.ts diff --git a/app/server/services/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts similarity index 100% rename from app/server/services/user/src/routes/account.ts rename to app/server/services/ts/user/src/routes/account.ts diff --git a/app/server/services/database/src/routes/profile.ts b/app/server/services/ts/user/src/routes/profile.ts similarity index 100% rename from app/server/services/database/src/routes/profile.ts rename to app/server/services/ts/user/src/routes/profile.ts diff --git a/app/server/services/database/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts similarity index 100% rename from app/server/services/database/src/routes/transaction.ts rename to app/server/services/ts/user/src/routes/transaction.ts diff --git a/app/server/services/database/tsconfig.json b/app/server/services/ts/user/tsconfig.json similarity index 100% rename from app/server/services/database/tsconfig.json rename to app/server/services/ts/user/tsconfig.json diff --git a/app/server/services/user/.gitignore b/app/server/services/user/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/app/server/services/user/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/app/server/services/user/src/db/connection.ts b/app/server/services/user/src/db/connection.ts deleted file mode 100644 index 151f0b5..0000000 --- a/app/server/services/user/src/db/connection.ts +++ /dev/null @@ -1,14 +0,0 @@ -// TODO: create connection and export it -import mysql from "mysql2/promise" - -export const db = mysql.createPool({ - - //hard coded values - host: 'database', - port: 3306, - user: 'root', - password: 'a', - database: "finus" - -}); - diff --git a/app/server/services/user/src/routes/profile.ts b/app/server/services/user/src/routes/profile.ts deleted file mode 100644 index e811bb5..0000000 --- a/app/server/services/user/src/routes/profile.ts +++ /dev/null @@ -1,30 +0,0 @@ -// profile creation route this lets us create profiles in the db , which can be used to group accounts together and track them. - -import { Router } from "express"; -import type { Request, Response } from "express"; -import type { ResultSetHeader } from "mysql2"; -import { db } from "../db/connection"; - -export const profilesRouter = Router(); - -// Create a profile -profilesRouter.post("/", async (req: Request, res: Response) => { - try { - const { name, description } = req.body; - - const [result] = await db.query( - `INSERT INTO profile (name, description) - VALUES (?, ?)`, - [name, description ?? null] - ); - - res.json({ - message: "Profile successfully created", - id: result.insertId - }); - } catch (err) { - console.error("Profile creation failed:", err); - res.status(500).json({ error: "Profile creation failed" }); - } -}); - diff --git a/app/server/services/user/src/routes/transaction.ts b/app/server/services/user/src/routes/transaction.ts deleted file mode 100644 index c03fe80..0000000 --- a/app/server/services/user/src/routes/transaction.ts +++ /dev/null @@ -1,107 +0,0 @@ -// transaction routes for creating and updating transactions -import { Router } from "express"; -import type { Request, Response } from "express"; -import type { ResultSetHeader } from "mysql2"; -import { db } from "../db/connection"; - -export const transactionsRouter = Router(); - -//creating a new transaction - -transactionsRouter.post("/", async (req: Request, res: Response) => { - try { - const { financialAccount_id, amount, description, sender, recipient, date } = req.body; - - const [result] = await db.query( - `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) - VALUES (?, ?, ?, ?, ?, ?)`, - [financialAccount_id, amount, description ?? null, sender ?? null, recipient ?? null, date] - ); - - res.json({ - message: "Transaction successfully created", - id: result.insertId - }); - } catch (err) { - console.error("Transaction creation failed", err); - res.status(500).json({ error: "Transaction creation failed" }); - } -}); - - - -// Update an existing transaction -transactionsRouter.put("/:id", async (req: Request, res: Response) => { - try { - const id = req.params.id; - const {amount,description, sender, recipient, date} = req.body; - - await db.query( - `UPDATE transaction - SET amount=?, description=?, sender=?, recipient=?, date=? - WHERE id=?`, - [amount, description ?? null, sender ?? null, recipient ?? null, date, id] - ); - - res.json({ message: "Transaction successfully updated" }); - } catch (err) { - console.error("Transaction update failed", err); } -}); - -// CSV Transaction -transactionsRouter.post("/csvTransaction", async (req: Request, res: Response) => { - try { - const { financialAccount_id, transactions } = req.body; - - if (!financialAccount_id || !Array.isArray(transactions)) { - return res.status(400).json({ error: "Invalid request payload" }); - } - - const validRows = transactions.filter((t: any) => t.errors.length === 0); - - if (validRows.length === 0) { - return res.status(400).json({ error: "No valid transactions to import" }); - } - - const connection = await db.getConnection(); - - try { - await connection.beginTransaction(); - - for (const row of validRows) { - await connection.query( - `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) - VALUES (?, ?, ?, ?, ?, ?)`, - [ - financialAccount_id, - row.amount, - row.description ?? null, - row.sender ?? null, - row.recipient ?? null, - row.date, - ] - ); - } - - await connection.commit(); - - res.json({ - message: "CSV transactions imported successfully", - inserted: validRows.length, - skipped: transactions.length - validRows.length, - }); - - } catch (err) { - await connection.rollback(); - console.error("CSV transaction import failed", err); - res.status(500).json({ error: "CSV transaction import failed" }); - - } finally { - connection.release(); - } - - } catch (err) { - console.error("CSV transaction import failed", err); - res.status(500).json({ error: "CSV transaction import failed" }); - } -}); diff --git a/app/server/services/user/tsconfig.json b/app/server/services/user/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/app/server/services/user/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} From e843b2ef6664bec919235cd22bf6cc9f7ae9316b Mon Sep 17 00:00:00 2001 From: Logan Date: Tue, 3 Mar 2026 17:24:54 -0600 Subject: [PATCH 122/204] updated README.md --- README.md | 128 +++--------------- documentation/product_vision.md | 120 ++++++++++++++++ .../resources/architecture_diagram.png | Bin 0 -> 503655 bytes 3 files changed, 137 insertions(+), 111 deletions(-) create mode 100644 documentation/product_vision.md create mode 100644 documentation/resources/architecture_diagram.png diff --git a/README.md b/README.md index e221936..2e13c46 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,26 @@ -# COMP-4350-Finus Team 6 +# About +Our project provides users with basic personal financial tracking and analytics via a web-based dashboard. -#### Members: -Roman Tebel, Jackie Mei, Deep Patel, Hoang Huy Truong, Logan Decock, Dylan Prabagaran +We target users past the age of 16, who are looking to gain better awareness of their financial situation. Our UI prioritizes intuitive design and meaningful analytics. -# Project Summary and Vision -Our project provides users with basic personal financial tracking and analytics on a web-based dashboard. +# Getting started -#### Target Audience -Our project targets users past the age of 16, who are looking to gain better awareness of their financial situation. Because of a broad range, our UI will prioritize intuitive design and meaningful analytics. +#### Installation +Our project is completely dockerized, so our sole dependency is docker. -#### Core Problem -Financial education varies highly among the general population, and some people find themselves in difficult situations because of their ignorance when it comes to savings and expenditures. +Our microservices are located in `app/server`. +run `docker compose up` in this directory to start run the backend. -#### Key Benefit -This project will serve as a means to make users more situationally aware of their finances, and an easier portal to personal finance compared to Excel spreadsheets. Our project would provide the user with everything they would need to get started after a simple interactive setup. This will alleviate the common headache of formatting, processing and visualizing personal finances for the less technical users. +The webserver is located in `app/client`. The same command starts the webserver. -## Core Functional Features - 1. User Authentication. This is a standard sign up and sign in system, which will be implemented via JTW. Our fallback is to rely on cookies should JTW fail. - 2. User Data Input. At the minimum, users will be able to manually input their expenses and savings. Ideally, users would upload CSV files of bank statements, as we can’t get access to an API of any Canadian bank (we’re not a commercial body). - 3. Visualization Dashboard. After processing user data, our app will visualize what users spend their money on and present statistics of their finances over time in various charts. - 4. Financial Goals. Users will be able to declare goals for savings and track them over time. - 5. Projections. With enough data, the app will produce possible outcomes for where users will end up over a period of time. This will visualize compounding savings and how expenses can affect the users long-term. - 6. Stock and FOREX Tracking. By using an open-source library to skim through Yahoo Finance and Oanda API, we will showcase users a minimalized view of the stock and foreign currency exchange markets. Users would be able to search for instruments, pin them, and view their history over time. - 7. Stretch goal: Machine Learning Integration. Through the Python submodule on the server-side, we would boost our projections accuracy and could provide the user recommendations on how they could decrease spendings. With ML, we would also automatically classify user expenses from bank statements. - 8. Stretch goal: Collaborative Budgets. Users would collaborate with other users and combine their expenses/savings for all features. +#### Usage +Once you have both ends of the project running you can access the website via http://localhost. +# Architecture +Finus was designed with a microservice architecture. +You can find each service in the `app/server/services` folder. -## Technologies - - JTW for user authentication - - Typescript for all JS. - - React.js relevant graph libraries for client-side UI in a browser page - - Node.js for server-side application - - Express.js for API and some server logic - - SQLite or MySQL for database - - Potentially Python for machine learning and heavier data processing on the server. - -## User Stories - -### 1. Visualization Dashboard: -#### User Story 1: As a user, I want to see the state of my income, investments, savings and expenses in order to better understand my financial situation. -#### Acceptance Criteria: - - The user is presented with a dashboard, which contains statistical graphs - - At least one of the graphs visualizes expenses over time - - At least one of the graphs visualizes the savings contribution over time - - At least one of the graphs shows the relationship between total income and where all of that money ends up going - -#### User Story 2: As a user, I want to see how I’ve kept up with a proposed budget over a period of time so that I can learn if I need to fix anything about my financial habits. -#### Acceptance Criteria: - - Users are proposed a feasible budget plan - - The budget plan takes into account the user’s income and expenses - - The budget prioritizes saving money - - The user is shown how closely they match the budget and whether they overspent within a defined period of time - -### 2. User Data Input: -#### User Story 1: As a user, I want multiple ways to input my financial data so that I can explain my financial situation with more precision. -#### Acceptance Criteria: - - Users can enter data regarding finances manually - - Users can also enter data regarding finances semi-automatically, by letting the app parse a CSV bank statement - - Users are able to set up profiles, which incorporate all of their finances - - Users can create categories for investments, incomes and expenses and assign transactions into those categories - - When bank statements are parsed, transactions are compared against all the historical transactions within certain categories in order to decide which category they fall under - -#### User Story 2: As a user I want to be able to edit transactions in order to correct personal mistakes and ambiguous bank statements. -#### Acceptance Criteria: - - Users can edit the transaction category for any historical transaction - - Users can edit the name of any historical transaction - - Users can edit the financial value associated with any historical transaction - -### 3. Projections: -#### User Story 1: As a user who has debt I want to input financial data related to it, and I want to see when I would be able to pay it all off with minimum contributions. I need to know this in order to see what debt I should focus on paying off first. -#### Acceptance Criteria: - - Users can input various kinds of debt like loans and mortgages - - Certain kinds of debt have additional input associated with them like amortization - - Users are presented with a projection of how much they will owe at every specified period of time, given minimum payments - -#### User story 2: As a user who wishes to save money, I want to see how my savings will compound over time in various accounts so that I can plan for my future. -#### Acceptance Criteria: - - Users can input financial information about their savings accounts - - Users can specify the kind of account they’re using for savings - - Users are presented with a graph that visualizes how their savings compound over time - - The projected savings graph shows both worst-case and best-case growth based on historical economic values - -### 4.Financial Goals: -#### User Story 1: As a user I want to specify a financial goal such as decreased spending or saving a certain amount of money by a certain date. I need this to keep myself on track with future plans. -#### Acceptance Criteria: - - User budget is aligned with the selected financial goal - - User budget recommends changes to user’s financial habits should the user stray away from their financial goal - -### 5. User Authentication: -#### User Story 1: As a user I want to create an account so that all my data can be loaded from various supported devices -#### Acceptance Criteria: - - Users can create an account with an email and a password - - User data is associated with an account under a certain name - - Users can log into their account from multiple supported devices (Desktop only) - - Users can log out of their account, and log in with a different account on the same machine - -### 6. Stock and FOREX Tracking: -#### User story 1: As a user I want to view the current state of the stock and the foreign currency exchange markets so that I can learn if certain stocks are performing really well. -#### Acceptance criteria: - - Users can search for stocks and currency pairs on the dashboard - - Users can view historical data of a stock or a currency pair - - Users can pin instruments - - Pinned instruments always appear on the dashboard with a minimalised view of their historical data - -### 7. Stretch goal: Collaborative Budgets: -#### User story 1: As a user who has family and friends, I want to add other profiles under my account so that other people can see their finances and we can make plans together for the future. -#### Acceptance criteria: - - Users can specify if some of their profiles should be treated as combined - - Users are presented with a secondary collective budget if they have combined profiles - - The collaborative budget takes into account multiple profiles - - The collaborative budget has its own financial goals which it tries to follow - -### 8. Stretch goal: Machine Learning Integration: -#### User story 1: As a user I want a more accurate prediction of my finances over time in order to have better situational awareness. -#### Acceptance criteria: - - Projections for user savings are assisted with statistical ML algorithms - - Classification of transactions is assisted with classification ML algorithms based on semantic embeddings (through SBERT, as an example) - +![Architecture diagram](documentation/resources/architecture_diagram.png) +#### More +You can read more about our project's features and goals [here](documentation/product_vision.md). diff --git a/documentation/product_vision.md b/documentation/product_vision.md new file mode 100644 index 0000000..e221936 --- /dev/null +++ b/documentation/product_vision.md @@ -0,0 +1,120 @@ +# COMP-4350-Finus Team 6 + +#### Members: +Roman Tebel, Jackie Mei, Deep Patel, Hoang Huy Truong, Logan Decock, Dylan Prabagaran + +# Project Summary and Vision +Our project provides users with basic personal financial tracking and analytics on a web-based dashboard. + +#### Target Audience +Our project targets users past the age of 16, who are looking to gain better awareness of their financial situation. Because of a broad range, our UI will prioritize intuitive design and meaningful analytics. + +#### Core Problem +Financial education varies highly among the general population, and some people find themselves in difficult situations because of their ignorance when it comes to savings and expenditures. + +#### Key Benefit +This project will serve as a means to make users more situationally aware of their finances, and an easier portal to personal finance compared to Excel spreadsheets. Our project would provide the user with everything they would need to get started after a simple interactive setup. This will alleviate the common headache of formatting, processing and visualizing personal finances for the less technical users. + +## Core Functional Features + 1. User Authentication. This is a standard sign up and sign in system, which will be implemented via JTW. Our fallback is to rely on cookies should JTW fail. + 2. User Data Input. At the minimum, users will be able to manually input their expenses and savings. Ideally, users would upload CSV files of bank statements, as we can’t get access to an API of any Canadian bank (we’re not a commercial body). + 3. Visualization Dashboard. After processing user data, our app will visualize what users spend their money on and present statistics of their finances over time in various charts. + 4. Financial Goals. Users will be able to declare goals for savings and track them over time. + 5. Projections. With enough data, the app will produce possible outcomes for where users will end up over a period of time. This will visualize compounding savings and how expenses can affect the users long-term. + 6. Stock and FOREX Tracking. By using an open-source library to skim through Yahoo Finance and Oanda API, we will showcase users a minimalized view of the stock and foreign currency exchange markets. Users would be able to search for instruments, pin them, and view their history over time. + 7. Stretch goal: Machine Learning Integration. Through the Python submodule on the server-side, we would boost our projections accuracy and could provide the user recommendations on how they could decrease spendings. With ML, we would also automatically classify user expenses from bank statements. + 8. Stretch goal: Collaborative Budgets. Users would collaborate with other users and combine their expenses/savings for all features. + + +## Technologies + - JTW for user authentication + - Typescript for all JS. + - React.js relevant graph libraries for client-side UI in a browser page + - Node.js for server-side application + - Express.js for API and some server logic + - SQLite or MySQL for database + - Potentially Python for machine learning and heavier data processing on the server. + +## User Stories + +### 1. Visualization Dashboard: +#### User Story 1: As a user, I want to see the state of my income, investments, savings and expenses in order to better understand my financial situation. +#### Acceptance Criteria: + - The user is presented with a dashboard, which contains statistical graphs + - At least one of the graphs visualizes expenses over time + - At least one of the graphs visualizes the savings contribution over time + - At least one of the graphs shows the relationship between total income and where all of that money ends up going + +#### User Story 2: As a user, I want to see how I’ve kept up with a proposed budget over a period of time so that I can learn if I need to fix anything about my financial habits. +#### Acceptance Criteria: + - Users are proposed a feasible budget plan + - The budget plan takes into account the user’s income and expenses + - The budget prioritizes saving money + - The user is shown how closely they match the budget and whether they overspent within a defined period of time + +### 2. User Data Input: +#### User Story 1: As a user, I want multiple ways to input my financial data so that I can explain my financial situation with more precision. +#### Acceptance Criteria: + - Users can enter data regarding finances manually + - Users can also enter data regarding finances semi-automatically, by letting the app parse a CSV bank statement + - Users are able to set up profiles, which incorporate all of their finances + - Users can create categories for investments, incomes and expenses and assign transactions into those categories + - When bank statements are parsed, transactions are compared against all the historical transactions within certain categories in order to decide which category they fall under + +#### User Story 2: As a user I want to be able to edit transactions in order to correct personal mistakes and ambiguous bank statements. +#### Acceptance Criteria: + - Users can edit the transaction category for any historical transaction + - Users can edit the name of any historical transaction + - Users can edit the financial value associated with any historical transaction + +### 3. Projections: +#### User Story 1: As a user who has debt I want to input financial data related to it, and I want to see when I would be able to pay it all off with minimum contributions. I need to know this in order to see what debt I should focus on paying off first. +#### Acceptance Criteria: + - Users can input various kinds of debt like loans and mortgages + - Certain kinds of debt have additional input associated with them like amortization + - Users are presented with a projection of how much they will owe at every specified period of time, given minimum payments + +#### User story 2: As a user who wishes to save money, I want to see how my savings will compound over time in various accounts so that I can plan for my future. +#### Acceptance Criteria: + - Users can input financial information about their savings accounts + - Users can specify the kind of account they’re using for savings + - Users are presented with a graph that visualizes how their savings compound over time + - The projected savings graph shows both worst-case and best-case growth based on historical economic values + +### 4.Financial Goals: +#### User Story 1: As a user I want to specify a financial goal such as decreased spending or saving a certain amount of money by a certain date. I need this to keep myself on track with future plans. +#### Acceptance Criteria: + - User budget is aligned with the selected financial goal + - User budget recommends changes to user’s financial habits should the user stray away from their financial goal + +### 5. User Authentication: +#### User Story 1: As a user I want to create an account so that all my data can be loaded from various supported devices +#### Acceptance Criteria: + - Users can create an account with an email and a password + - User data is associated with an account under a certain name + - Users can log into their account from multiple supported devices (Desktop only) + - Users can log out of their account, and log in with a different account on the same machine + +### 6. Stock and FOREX Tracking: +#### User story 1: As a user I want to view the current state of the stock and the foreign currency exchange markets so that I can learn if certain stocks are performing really well. +#### Acceptance criteria: + - Users can search for stocks and currency pairs on the dashboard + - Users can view historical data of a stock or a currency pair + - Users can pin instruments + - Pinned instruments always appear on the dashboard with a minimalised view of their historical data + +### 7. Stretch goal: Collaborative Budgets: +#### User story 1: As a user who has family and friends, I want to add other profiles under my account so that other people can see their finances and we can make plans together for the future. +#### Acceptance criteria: + - Users can specify if some of their profiles should be treated as combined + - Users are presented with a secondary collective budget if they have combined profiles + - The collaborative budget takes into account multiple profiles + - The collaborative budget has its own financial goals which it tries to follow + +### 8. Stretch goal: Machine Learning Integration: +#### User story 1: As a user I want a more accurate prediction of my finances over time in order to have better situational awareness. +#### Acceptance criteria: + - Projections for user savings are assisted with statistical ML algorithms + - Classification of transactions is assisted with classification ML algorithms based on semantic embeddings (through SBERT, as an example) + + diff --git a/documentation/resources/architecture_diagram.png b/documentation/resources/architecture_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..01b6dd1859d9ef56e8042f82d7d9803e05f3e5a1 GIT binary patch literal 503655 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

Hr(6!riFI~V*K{yfo86n=1nH~@nlg4A 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 A`~Uy| literal 0 HcmV?d00001 From d4e22c7481193a0868e2a35085f1a5cf6d8d0881 Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Wed, 4 Mar 2026 14:28:01 -0600 Subject: [PATCH 123/204] Fixed bug when composing docker for client --- app/client/package.json | 2 ++ app/client/src/components/AccountForm.tsx | 4 ++-- app/client/src/components/DashboardChartSection.tsx | 2 +- app/client/src/components/IncomeForm.tsx | 4 ++-- app/client/src/components/TransationForm.tsx | 8 ++++---- app/client/src/components/csvread/CsvError.tsx | 1 - app/client/src/components/csvread/CsvFile.tsx | 2 -- app/client/src/utils/ParseCsv.ts | 7 ++++--- app/client/src/utils/ValidateForms.ts | 6 +++--- app/server/services/ts/api-gateway/src/index.ts | 10 ++++++---- app/server/services/ts/auth/src/logic.ts | 2 +- 11 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/client/package.json b/app/client/package.json index d164fd1..ec1aae1 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -17,6 +17,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", + "papaparse": "^5.5.3", "radix-ui": "^1.4.3", "react": "^19.2.0", "react-chartjs-2": "^5.3.1", @@ -30,6 +31,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^25.2.0", + "@types/papaparse": "^5.3.16", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.2", diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index 52c550f..f8071c1 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -1,10 +1,10 @@ import React, { useState } from "react"; -import './UserForm.css' +import './userForm.css' import {postUserAccount, putUserAccount} from '../api/Account.ts' import { type Account } from "../types/AccountType.ts"; import { validateAccountForm } from "../utils/ValidateForms.ts"; import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput.ts"; -import { accountCategory } from "@/enum/accountCategory.ts"; +import { accountCategory } from "@/enum/AccountCategory.ts"; interface popupProp{ toggle: () => void; diff --git a/app/client/src/components/DashboardChartSection.tsx b/app/client/src/components/DashboardChartSection.tsx index 1860a01..e8fd81a 100644 --- a/app/client/src/components/DashboardChartSection.tsx +++ b/app/client/src/components/DashboardChartSection.tsx @@ -71,7 +71,7 @@ function DashboardChartSection() { default: fetchExpensesData(); } - }, [activeChart, selectedPeriod, fetchExpensesData, fetchSavingsData,fetchIncomeData]); + }, [activeChart, selectedPeriod]); //Temporary test data for expenses - delete once API works. This is just to test graph components diff --git a/app/client/src/components/IncomeForm.tsx b/app/client/src/components/IncomeForm.tsx index d34e3bd..0d44da1 100644 --- a/app/client/src/components/IncomeForm.tsx +++ b/app/client/src/components/IncomeForm.tsx @@ -1,6 +1,6 @@ import { useState } from "react" import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput" -import './UserForm.css' +import './userForm.css' import {postIncome, putIncome} from "../api/Income" import { validateIncomeForm } from "../utils/ValidateForms"; import { type Income } from "../types/IncomeType"; @@ -92,4 +92,4 @@ export default function PopupForm({toggle,setIncome, addIncome, edit, selectedIn

) -} \ No newline at end of file +} diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index 342df78..44cc6b8 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -1,13 +1,13 @@ -import React, { useState} from "react"; +import { useState} from "react"; import CsvUpload from "./csvread/CsvUpload"; -import './UserForm.css' +import './userForm.css' import { getUserAccounts} from "../api/Account"; import { validateTransactionForm } from "../utils/ValidateForms"; import { postTranscations, putTranscations} from "../api/Transaction"; import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput"; import { type Transaction } from "../types/TransactionType"; import { type Account } from "../types/AccountType"; -import { transactionCategory } from "@/enum/transactionCategory"; +import { transactionCategory } from "@/enum/TransactionCategory"; interface popupProp{ toggle: () => void; @@ -172,4 +172,4 @@ export default function PopupForm({toggle,setTransaction,addTransaction, edit, s ) } - \ No newline at end of file + diff --git a/app/client/src/components/csvread/CsvError.tsx b/app/client/src/components/csvread/CsvError.tsx index 2e8925f..2a15960 100644 --- a/app/client/src/components/csvread/CsvError.tsx +++ b/app/client/src/components/csvread/CsvError.tsx @@ -1,5 +1,4 @@ //component to show parsing errors -import React from "react"; interface CsvErrorProps { message: string; diff --git a/app/client/src/components/csvread/CsvFile.tsx b/app/client/src/components/csvread/CsvFile.tsx index 0e5a865..916cfa9 100644 --- a/app/client/src/components/csvread/CsvFile.tsx +++ b/app/client/src/components/csvread/CsvFile.tsx @@ -1,7 +1,5 @@ //This displays file name, size, and a remove -import React from "react"; - interface CsvFileProps { file: File; onRemove: () => void; diff --git a/app/client/src/utils/ParseCsv.ts b/app/client/src/utils/ParseCsv.ts index 139c413..0efbd7e 100644 --- a/app/client/src/utils/ParseCsv.ts +++ b/app/client/src/utils/ParseCsv.ts @@ -1,7 +1,7 @@ //this utility parses csv files using papaparse // Parses CSV, normalizes rows, validates them, and returns TransactionDraft[] -import Papa from "papaparse"; +import Papa, { type ParseResult } from "papaparse"; import { normalizeRow } from "./NormalizeRow"; import { convertDraft, type TransactionDraft } from "./ConvertTransaction"; @@ -12,7 +12,8 @@ export function parseCsvFile(file: File): Promise { //uses skipEmptyLines: true, dynamicTyping: true, - complete: ({ data, errors }) => { // handle parsing errors + complete: (result: ParseResult) => { // handle parsing errors + const { data, errors } = result; if (errors.length > 0) { reject(errors[0].message); return; @@ -26,7 +27,7 @@ export function parseCsvFile(file: File): Promise { //uses resolve(drafts); // resolve with array of transaction drafts, each containing validation errors if any }, - error: (err) => reject(err.message), // handle file read errors + error: (err: Error) => reject(err.message), // handle file read errors }); }); } diff --git a/app/client/src/utils/ValidateForms.ts b/app/client/src/utils/ValidateForms.ts index 42b4c33..3d1e088 100644 --- a/app/client/src/utils/ValidateForms.ts +++ b/app/client/src/utils/ValidateForms.ts @@ -1,5 +1,5 @@ -import { accountCategory } from "@/enum/accountCategory"; -import { transactionCategory } from "@/enum/transactionCategory"; +import { accountCategory } from "@/enum/AccountCategory"; +import { transactionCategory } from "@/enum/TransactionCategory"; export function validateAccountForm(name:string, accountType:string, balance: number, csvFile?: File, subtype?: string, interest?: number) { let result = false @@ -43,4 +43,4 @@ export function validateTransactionForm(account_id:number, transactionType:strin export function validateIncomeForm(name:string, income:number){ return name && income > 0 -} \ No newline at end of file +} diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 4c54506..20344da 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -1,5 +1,4 @@ import { PORT } from "@/port"; -import { buildCorsConfig } from "@/expressUtils"; import { onExit } from "@/hooks"; import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; @@ -8,10 +7,13 @@ import cors from 'cors'; const app = express(); -//app.use(buildCorsConfig(undefined)); +const allowedOrigins = (process.env.CORS_ORIGINS ?? "http://localhost,http://localhost:8080") + .split(",") + .map((origin) => origin.trim()) + .filter((origin) => origin.length > 0); app.use(cors({ - origin: 'http://localhost:8080', // vite dev server so that the client can access this API + origin: allowedOrigins, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] @@ -170,4 +172,4 @@ const server = app.listen(PORT, () => { }); onExit(async () => await server.close()); -//process.on("SIGTERM", () => server.close()); \ No newline at end of file +//process.on("SIGTERM", () => server.close()); diff --git a/app/server/services/ts/auth/src/logic.ts b/app/server/services/ts/auth/src/logic.ts index 1d67e94..4c15ad8 100644 --- a/app/server/services/ts/auth/src/logic.ts +++ b/app/server/services/ts/auth/src/logic.ts @@ -29,7 +29,7 @@ export async function signup(body: SignupBody, res, pool) { const hash = await hashPassword(body.password); await createUser({ ...body, pw_hash: hash }, pool); - res.status(201); + res.status(201).json({ ok: true }); } export async function login(body: LoginBody, res, pool): void { From 96a94f2938c5277f81b920beb18b29a25164b852 Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 4 Mar 2026 14:58:56 -0600 Subject: [PATCH 124/204] improve pre-commit hook script --- hooks/format_ts_dir.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hooks/format_ts_dir.sh b/hooks/format_ts_dir.sh index 222863a..a449c26 100755 --- a/hooks/format_ts_dir.sh +++ b/hooks/format_ts_dir.sh @@ -1,18 +1,18 @@ #!/bin/bash #first argument ($1) is the directory to format the files of. -changed_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E $1/) -if [ -n $changed_files ]; then +changed_files=$(git diff --cached --name-only --diff-filter=ACMRd | grep -E $1/) +if [ -z "$changed_files" ]; then exit 0 fi -echo "files: $changed_files" normalized_files=$(echo $changed_files | sed "s|$1/||g" ) cd $1 -bun eslint $changed_dir_files +bun eslint $normalized_files if [[ $? != 0 ]]; then echo "❌ Failed to format files" exit 1 fi -bun prettier $changed_dir_files --write --ignore-unknown +bun prettier $normalized_files --write --ignore-unknown + git update-index --again From 15119f20dc6c7a2a587bcf5d7549fd0c3219e2ef Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 4 Mar 2026 15:38:50 -0600 Subject: [PATCH 125/204] 100% auth backend coverage! --- app/server/services/ts/auth/src/parsing.ts | 7 +- .../services/ts/auth/test/parsing.test.ts | 89 +++++++++--------- .../services/ts/auth/test/validation.test.ts | 19 ++-- app/server/services/ts/bun.lockb | Bin 116880 -> 116880 bytes app/server/services/ts/package.json | 2 +- 5 files changed, 64 insertions(+), 53 deletions(-) diff --git a/app/server/services/ts/auth/src/parsing.ts b/app/server/services/ts/auth/src/parsing.ts index 9820790..33e4099 100644 --- a/app/server/services/ts/auth/src/parsing.ts +++ b/app/server/services/ts/auth/src/parsing.ts @@ -29,9 +29,10 @@ export function parseSignupBody(obj: unknown): SignupBody { "password", ]); - const idInt = Number.parseInt(obj.age); - if (Number.isNaN(idInt)) { - throw new Error("age must be a number"); + const idInt = Number(obj.age); + if (Number.isNaN(idInt) || Math.floor(idInt) !== idInt) { + const error = new Error("age must be a number"); + throw error; } return { diff --git a/app/server/services/ts/auth/test/parsing.test.ts b/app/server/services/ts/auth/test/parsing.test.ts index 3605f62..50dca43 100644 --- a/app/server/services/ts/auth/test/parsing.test.ts +++ b/app/server/services/ts/auth/test/parsing.test.ts @@ -1,57 +1,62 @@ -import { describe, expect, it, beforeEach } from 'vitest'; -import { parseLoginBody, parseSignupBody } from '../src/parsing.ts'; +import { describe, expect, it, beforeEach } from "vitest"; +import { parseLoginBody, parseSignupBody } from "../src/parsing.ts"; let signupBody; let loginBody; beforeEach(() => { - signupBody = { - username: 'username', - email: 'email', - first_name: 'first_name', - last_name: 'last_name', - age: '21', - password: 'password', - }; - - loginBody = { - email: 'email', - password: 'password', - }; + signupBody = { + username: "username", + email: "e@ma.il", + first_name: "first_name", + last_name: "last_name", + age: "21", + password: "Password123123", + }; + + loginBody = { + email: "e@ma.il", + password: "password", + }; }); describe("loginBody tests", () => { - it("Should throw on non-numeric ages", () => { - loginBody.age = "twenty one"; - expect(() => parseSignupBody(signupBody)).toThrowError(); - }); - - it("Should only accept integer ages", () => { - loginBody.age = 21.25; - expect(() => parseSignupBody(signupBody)).toThrowError(); - }); - - it("Should throw on missing field", () => { - delete loginBody.username; - expect(() => parseSignupBody(signupBody)).toThrowError(); - }); + it("Should accept a valid signupBody", () => { + parseSignupBody(signupBody); + }); + + it("Should throw on non-numeric ages", () => { + signupBody.age = "aoue"; + expect(() => parseSignupBody(signupBody)).toThrowError(); + }); + + it("Should only accept integer ages", () => { + signupBody.age = "21.253"; + expect(() => parseSignupBody(signupBody)).toThrowError(); + }); + + it("Should throw on missing field", () => { + delete signupBody.username; + expect(() => parseSignupBody(signupBody)).toThrowError(); + }); }); - describe("loginBody tests", () => { - it("Should lowercase email", () => { - loginBody.email = 'EMAIL@MAIL.COM'; - const parsed = parseLoginBody(loginBody); + it("Should lowercase email", () => { + loginBody.email = "EMAIL@MAIL.COM"; + const parsed = parseLoginBody(loginBody); - expect(parsed.email).toEqual(loginBody.email.toLowerCase()); - }); + expect(parsed.email).toEqual(loginBody.email.toLowerCase()); + }); - it("Should trim email", () => { - loginBody.email = ' email@mail.com '; - const parsed = parseLoginBody(loginBody); + it("Should trim email", () => { + loginBody.email = " email@mail.com "; + const parsed = parseLoginBody(loginBody); - expect(parsed.email).toEqual(loginBody.email.trim()); - }); + expect(parsed.email).toEqual(loginBody.email.trim()); + }); + it("Should throw on invalid email", () => { + loginBody.email = "abc"; + expect(() => parseLoginBody(loginBody)).toThrow(); + }); }); - - diff --git a/app/server/services/ts/auth/test/validation.test.ts b/app/server/services/ts/auth/test/validation.test.ts index 2b96c00..5a5a996 100644 --- a/app/server/services/ts/auth/test/validation.test.ts +++ b/app/server/services/ts/auth/test/validation.test.ts @@ -2,10 +2,10 @@ import { vi, describe, expect, it, beforeEach } from "vitest"; import { validateSignupBody } from "../src/validation"; import type { SignupBody } from "../src/types"; -vi.mock('../src/config.ts', () => ({ +vi.mock("../src/config.ts", () => ({ MIN_PASSWORD_LENGTH: 8, MIN_AGE: 12, - MAX_AGE: 100 + MAX_AGE: 100, })); let signupBody: SignupBody; @@ -13,9 +13,16 @@ beforeEach(() => { signupBody = { email: "testemail@domain.com", password: "PASSWORPASSWORDDPASSWORD123!@#", + age: 15, }; }); +describe("Happy path", () => { + it("Should accept a valid signupBody", () => { + validateSignupBody(signupBody); + }); +}); + describe("Age validation tests", () => { it("should reject ages below the minimum", () => { signupBody.age = 11; @@ -28,21 +35,19 @@ describe("Age validation tests", () => { }); }); - describe("Password validation tests", () => { it("should reject passwords that are too short", () => { - signupBody.password = 'PA12'; + signupBody.password = "PA12"; expect(() => validateSignupBody(signupBody)).toThrowError(); }); it("should reject passwords that have no letters", () => { - signupBody.password = '11111111111'; + signupBody.password = "11111111111"; expect(() => validateSignupBody(signupBody)).toThrowError(); }); it("should reject passwords that have no digits", () => { - signupBody.password = 'aaaaaaaaaaa'; + signupBody.password = "aaaaaaaaaaa"; expect(() => validateSignupBody(signupBody)).toThrowError(); }); - }); diff --git a/app/server/services/ts/bun.lockb b/app/server/services/ts/bun.lockb index 69a8a97c581dd877522b29ff0db90a978cfa6fe9..d4409915ab3a43d40f07100c3595816da40ffb17 100755 GIT binary patch delta 15263 zcmeHud3a4%+xK2u4mshNGlv8*g@gn-i6keeDLF!dv}Xd{p~)>^47tehy2?0SM*(&C zzGh(cQ61?9O8VQN)ZphD9iEe6M?ca$t=<$+7nJ9KlA|L>*z>7;ye1dg^Co9a${Jo! zI3h4BZ=ys2OA>!jqhXPkpXytEP--GCBX6vIgd}AZWKOW>j*wcnkfestHwLA#l0X}S zK4`A$&x3kFJ^)Jk4?xkYLx7U~tDrQd<3OpMVW4E!8x(d$ju1`Z4T|x17}NsC_s!G> zu7Og8Q=rtqZcSbVN*&DurH-b6Qb!}T@_4O0Qj-Hfseb<0+}sJ{BxzlsB>722l4C9y zieNlw6VOCZjI$#`(>K-hvZlY=UXn23j+3BGK{tZ>g3i_CF`($n5eJGvb+p&yI-2~T zom%fCXp15ktOtXj9kW5Hq0t(R1EmgvLCK+pn*OH%RlW>LX*V`=LN+E_`VryN2z-G9 zjmQSGntnMM1%V@TGqT5IjLr%~W)x(NnV6N2G=*Ie4T)2@8c{ANO~Ef=Y8vf_K?BHJ zKq=oBbXD_xOh&eSct$~C%LrBe2Pg$F8k8LJ05yT8z$vQV5|nZ)t032&S13s((UJt6 zqaYgNPZ1_ULG&|=>Ukt&8sb))IevVp-)CP02Cg!1z^ky&BV7f+kqG5_WvA3$nFcubOjgUrXO`b^h ztdF`t*JyT&Ak)x~DhQmMF(y~a$j=^^fdQWlJq578uR1byV^jxASzmsyW>Yyhmb=ty zRn*9;#`8EU=499&4WzMW@_{T^c3GIP>SCQeQnTg8nqV$X6KI0 zlxhu7?Ye0CHzA{+BF7Cd)B){efe7-Y9Q*Jb#Az?U_5s=s6{v%N1a(}Rfl@~!va-en z+6yEkh*Cb%o;M;8t1M@r>OiVS#|7q&9iCAL<6Qd~d!dv%NG;DQm{4fXEh@maGk!u= z{$vR|T<&Nz)^o6GPz*|rT>&Kr21QAdKj=Rb)zmu-N{em|D0XYdYEWNLeWo3dX{4T) z8J*CcACkf01%^`31IgY3=E$O(R!&#JE6q&OUR#tNHZp!==e}&dMF%c6q7% z#K@y(cD+39u5U%1v%L-vAAY*^5BdB`&K^_p!!E3ID=CZ&QNb5S4PNDKHbz70 z4@u??{LIEhkl>g@;tgEQ@>vfa>u)jC#PD=i+Un!UE8AG))t=m?t;P5g^dU-BG$gn2 z;85&>@sqm*lIv*%;YGlCUi}oXA(>%aA%ks>w^-&4vw-EZpQ5!R~TnV6?GS zQ#CFpidjx?%4Y{!UmhD|G4@0rQB$>eRU@-;6{Ikw(-K!R zyTpwhqK&Qn)G!#Yax)w3kRrLsGlG@!6CI-258T)>+7OQOW>0SF7-5(OuD8M+2G>{N zY9p7`(gENGE4uf!;|m27yy+EPtlE4X(dZqf2YO zbUHZIm*e2n-s-i{+ZzN<)x8Z)ZSOWXwMJlD-Gi6FsdoFpsdlvkblo6uihuI^0lYHQ zVyFm|qzryKG{Vp}NRkqHao0#EFhdp9MQ|w!7t(M z`x@Mf3Kttpy10l)Cosbml?!rH?R7LbRrm2TU58G3BXhuID>Z%qm!)vYo%M#xo^ftn z^cn@=RHsgXQypj*s_R|{XXB^4MmmF`JVLs9nvJz_avlh&1|_I56%sAG8cHgcL+S~s z7BBHK8-ImF8Rx_k{LRKLV4q3ylrZj+XpuLE@z@lL+$5aOPO%tY3&+GLjU`|uoQ8z+ zvBQO`8=SG6F&VBA#(v=XDpu6V3P{}`!8xq-8(q1}P>UQK!DB(zMex}mbt8G@P>bQ^ zNJ{Uxp%HRfB%d8+kvB#0N|2wTxQhiF3)WyhH@Qa`+MvBe;*8V4QLI>QEzHJykknWc ze9UrKcODyUk*9a(vq4UG=anEHJ-ACZi!lv*L%dRtBHs*&_5g`fy@x$`WjBi)_X2n6 zZjr?cJQn1}3w(BWiy<0=rv~{NIQUB&7)E(NBKw>ogn%6z;Ci7c`0poh01VVY@HF^pBcd2COMu?|lD>JAGNIkpe4 zgmOKU+KeL`444V3hgKj(KvMS@YO??mO$|2aMrOk)NIjHMb&pXuu^O}|8WSLqm69cf z5=fnSv0H@kC^#gLqpsrZ6G+HWBtwH%Wmqh7&`)$f>C~yW!ghXQ}@sh^qT$5_@gjQkB5Y#cF z6ITOVxlkj@>XPFI@YsG9!%`@F^Em$q<9%?*A94?=)BzDtNLX$d?&*-Q5vaLuC?$#N z$hYEoWt_#>71uZzbQCA_Gs~|g@Ywzq<31>|1(3*fb=_{$ zB)AR-9zr4qRLj=53Zi=IOy)pRL#}EP=ByRTbfz*~A&D2ajW9OEbx?@fIM(<8NNQI! zW=kPatjc-O@D-#;Ufejs&^%F+(s*%HgrR1VBn?rxba2TEw;Nn?b#ps=^&bvXme8_;*Q*o=x%WPN!DM@KEEQQVn)HU4{sJc(V z#Vfj2L$t1qx!^jZel6ahiP^Xn5{(j86He22hHw{~#n^nP+M&8rO&ZE)L%9x08V)0G z(A{jj1c`FK0i{rB-(iJLMY#5pRs*;YkPZS2I0VoiQ5iTw0{(taYd}5@kbMO}e}6`) zekDNk6kq_V0IK&1uB<^6<{7OC?sG*_DOJ3r$$v%3?uuqdlsdSo$$v&0qU@p8KNv}( zh`!e<{2BE^SsiNif07pcPdE7AY_EtS@FsPR(GKWo=Le}$C7W9HfTBS@R(S=mtj7}C1=CZ0Oq%2N6I`8J+h zV&bL}D|?Zrl-RiQTPFS?qzrC+%f@#@8uONw4d>;Ma=3{%;Z~N#?cBya7MS>HNZH(T zfsI!{nzq2o?ED0zNefLpaG{lr=93rNc$-BgeihOf-gc3V=!BJx z=d%Q&Ta4%+74Y!IHde?>aGb#J;y96aUt(h~@g+D;;@{yona3=(u_=5djxY1aI8NmW zZ`;^3z8=R{ID5y&ig*f+4!#Y?>D;&s@hn3;%dG4*UcSu6Ugr(owXqr8j^jW0K^))U zp37})CLfREEPevV*}T~b8+(&a#&Hg>!m*gQU1?);`E(rT@ryXl=fSINtc1^6g&5vL z46Ce+^YHgo6~Ct!yPXuCuXKJOjt~csY)%d4u&fwuakre4ii0aV_`! zz{b|`@i?yMCqBS@Y(Q?DwaObLzVvJoZ?nc*GK64Wa ztE}=iiLa_k;$fRj-0+E2E|vJePm=f@h&v(fl(>8@i7(z_;#udca+$=pL+rcN#9hx@ z<#LH_a|z1M+S=vI5e6-gYD6D>d=rjaGJ$Uxc(9 zQq(3ZJIrToLf-Abm_j$f5Eb|dSzTUixP*^XH&$LK;j$Bm^}SCGb(TG<6&4r%co zWc>~+`;6OnU|sD+)W-sD9g!uMa*?s;P(&vy;_gUFPzJ4F#JB;`~w6gDb%7=*W2;zf;pE^E5e2~U` zWMx0{a!8AhBEJ1r_HSabO=!T9XMNj#?#5kfRD-t|Zl_c)0Vk67hejNgM; z0Wt2VRW>rd^k@>FbP7R2tj+k($B?Y25!Z36Y-0R_<4OE;h^7jwT#xY~70A|)5m|+m zHQ>e*i1Q30J7HywcsZmykeXClnJc$fBF?jj43azdJc&4~O#H%0EA!;dP9aW6Gfr8V zH?M-U?h_O5a@xw8@ad-!=Q$I<1<98Oe~dWKoA|cE6us^ zPiLNf--E|rb2fOo;ns&&-EPMnjhxuS(&B3^WJ99_-3?b3vMwPnRD@=Irf2*3}(Pz{xp4z_;dY#4(lZzi5_@4AuhWk%xJsw-Od5Gx_|pFO(qz5@&sE92 z$otNQ6?no|Uys7&*>9&q`K$N8FWvEQcepFv*3n-xctJtXjcQ*py)A1Lm8jih`D;b= z%q>OJwE`uU?$**Y9bIt~OLuk=qKcY`)QZ|_MRb=x6+HE-KS`k*%xvhWdG%?EdXrco zngy^%uIo|cjG{oT3H(Yem5Z1F#*3n%>IQTNdH^o~J%L_8Z=eql3s`}EKpZdthzAmY zfxsYu0!;#v0Sa&kFccUDqyiM+i$FR+ccSzNrYpmsM}VOaSBp*Um~XF*aBUN?8Q21B z1-1d(fl^=xuoKt?lmSbDWx%_@a$p6pQk-bVL{S!6y$jp}=#k4Cz)WBkK)3Cy0X%Y- z=%L7K0NvWtV}f@9dj3Gq5EcVVfTh4Y06mA`zybgdH>5eh6yRlG8ZaKn0dSLDB+;$- z=RgIp4>$m<12zL&0D2aB4Y&^60KNq1LGCBOIp8zkW8e^Q7@)^noq=GWJrD%60Y;*1 zA@CNk0GJ1)OCl|hg#^-*8+tZDnL<~Tqku&KJxD169Kb6;K2RVY2Qss(H&`E_3D6Yq z1^fhS$Gk;)5DPO%keN6U$lArxE%LVjJte*fgrdj{bO5p;=KywK6fhbX4$u>l6~J_W zo??xMt^immE(WnqUHU>51K5E{z*K-zI}_LotOux!unQ;$_K5BsSckw5(Mns0G$yp* zK7!l=v^hX)lLDcD_CX(CwyXo|Bp0m*KL(&&8VC#nD8DGrDEBr19{^ABhfP8v`#bV0d0pu3_y$GJtfCe}n&_l@tp9PEnXcEX(vL~L>q!1WKudPr4 z`9BsQm)g~RN|TZg&;-pDhE6QoNZpXdIx(RW8(u^MLkm@3v|GTJ0_52?U^_q&&|;;5 z+zadmsKq@%xh7M#c>*3lU4SNzCi5%61+qWrm%vTH2-z8sX@&g=#Yuoc@hj-Rfqwx% z07n7J&4<8!fPDWN_y(X{JrA4(&HyKX3V>GM5#TUz5I6wr2WZG>6_R7r?lHiho>)`o zmB1;0mg;Gs3Lv9%z$Y5L0Qwp5DL{*bBGaS#0{l(jDsUN~HZK8JfE&Oy;5u*%_$NRP z6aO{(m+k>~f!n|xfTDN+d=Gr5fkx>k;1}R$;1Tc`cmj}I3xvnoF`HJ?Tg$Gy+dL;@v>q0orw_ z4Vos(3z`brk*GZ{K+g^2OVP7N^}^pLkM0C@tUE;w{?X-`|pRJI0f1+)an zA(Cl?X#Sr*ZS?dh%`ndwh~9 z{oLcJ`vRGz1;ZpnePgmA`nj|#dvHmIn6oJ97}O~!xTExitJsISKKeVD`fFRT=oAz} zbzR)Vw^=Mi9O%v3%gJuyL2owLM}J{AG5wpTKhMwj3FhQYCj{#z()utDd5W8;8;6GV zSA>1TUh97$?{&Xwx97Qujj+_>lb5=Q8xXrZKTiEEk@I4d$>z^;pzL45T0qkUR=ZJ9Q@%eU}81jEr08;HBu+Lwj+xVx+G z2d38#nvig^>nDv_fI2afpS$?2FAMMwfJIXbXW8@^HVdcE!4Rer(&3wTv9<-p?O6wH3X>UrdQM2XYHzCZGGp|9xOpM`|h_fxO-3ug8GZO0n_sR%7N2>-AT zpu~hhXm{bg{HoF0hbbi?R1(lkY(^WQk~ z?R=UvSX*5(vYB{-x;|52;R=g)y*utXcX#-ZYKwWzM8^TNAhb42?mh3myLsaAYKz@k zzlWp;eOld>yH%H*64M4?L0r=eF26RickrT12dfRf7u#SU8=8w#=)*^UiM?uN{U%4+ z+#OJD7}9)EJVMl7dw1lfuX_4vNqP5b1O3JLOt&Y-dcKW+sxBGVT=b{9`m6NqN4wYh z;qa*5)fW28_A{L}=DgUT>!j+E51Na2P}f_3J)cK>(_&Qd(wWs3=f#gq&ZUDCLPXqtei^=|}1`<>dg@~s_Hsx6T40~4^O^jFpQ zHg;>6I&aCVPFTBL!fB`YNi0u5_xii-2}>V5=(eKjw`v3Z?e|YZ_C78C-o~m+;@XJ^ z=sr|`U%s@ZQ)l!EPfk9mw$ML#H+k zOw}cG1H~28^-;f@kQ_Ze@_)~Nm9(td;zw8EKN1`32D5mOhBMu`k=Sj22@`WhqMG*2 zi{nsH|C?j4?@hz%$C-lGwo|w`0Ex1-|;kBF3O1z|4;KYlYN>6oPf7~Ez^lzO{ zFHv@m&OyPQr3GC@-E7qT0!yDR0Q9${wboHr{}!R%3%xJbi@G0%5_3=prC!SjF${Hm zp1&`Rju7+7LjS(utN1B3;u4pXscnP>bBMCKK$zjyC9 zF?MTyt#&-KhB1F(?#I9xURwM0l@%1kjrdec8u@T@2?UY)UZ14?jVL>?}(h>xkR{*6S|D|cVYPr12Pv49Qk2`%cmKP0#G zhsrZK*(eD^tC+o`7U5%qho4!*=AoeXEFvCqsQ%%{zb>_KUAg|tXw>K!)R{chKMk3? za(m*h)6OKKgs!O2R;y@%NANhyM5}N3-<)~5a9jUMXT>6zPDJ`=Cfhom9B+W-l=(1yq+4 zdy4}pte?#Kh`K}AQh9bC@xc(qg{wnwW?IxoJc1~f_7&|hfxq3uljk$Q-8Qb zGl&@L#)$NxxMmBERj=6=FX(pU{O)8ASaiWv9mZCjg#iCTSa>0qpEKw1{&B2$Li5$g zD%^&#em>8cFZ61N6R!+|U-~C28Hbu3?X%ePM`i4lYsm|7;uI`om4?W*1}sWNyl)N= z{b@XI3=lb~2v8l55V=``IIE4*J<q|KDdA-QjQTr{qk~X2@SK5SU>aIX z9VAx3GF1PHW^~kond8r0A0(5nv z*h(A2;Y3j<9rT}xqE$KzaOP2%G_)kTbz@#W`j;;ymt)xWs`NL|uD17}-dx!G>-H_- zfeY#_Z|P{?ZbnvSeWsUSg0D~yTF;_x1{&5sqDtBS!0^ts`)$y07led-K#KOvz!2*W zYb#yf4Sk-s(n6$PlEsaT--YIpBH|IMXrIX%>Kmbp?v+=R$jn4He;g03lKh7a^jW<> z*ozUvaX!+0)eE(q`@d?$=TG;Ba9XL)al@j!|0m<8rG)-jnl0gcTztiXw3B6u|OE^~}xyBmw}DR>Nd;5FtfW*=bw9`w~{euh0SbL>lc z#R>RIt3Yf%z($F%A~wB_@+DwkL1Df4$=&xuui*OR=au!t9_Cq$LhVVVkjAc!av3&Xe0L(qBKmY&$ delta 15131 zcmeHu33N?Y+yB{zOK!Lb5}6DkHRqBb_eSIrO_7`gL2}I`5y>T{B!-~P)iF|2Y;0OX z(N+{im6*jWF(s(CYG`$!^sUlXOW(Kc_j}GDzHilkegCz-Z~fQ$uimwO=Nb01_kNze zpS{m;&skJdb5T*vZI9&qX|CM#oLost^pPZQNpe@>Pz%%nxdG_4Mv`O(&2eS8CXSsX zS)s2-df3$l{nS^I0zfCH9!)&cz%b{5cTx}5E=$rm*`%)L+V@F6Hw zcnht8buR=ZN5TWO23zSgEi=W1exwt6z3rf$uv-I4j*cDU%BJ#SZ~j>|Yrdl zGMkt(agu9{B&Fx1PIYCCk)}15r25dOfl|EvKpTQa>G~F+aMfK`*FO%DB!uF=1xogn zpyci$P-DTs>)J)2k+}FWS1f`&jLEi<%Sh@>!{RCZ~sO#;Z zekk_`Z36nQc9>Pr>!2j>2ZfLBm7wU;JxiB|>2ee(+4+OEhzIkitt4Ue+*d%Up}jg? z2}*`;P;w|m*T104Z9yruCZ$fzknqdmDK#49caD&x=b-fgrP`0|8l4GB~t?9-Ebto0~pH8lRpwjqKm;p(S`9-7X?p3t(JMNM6c>EGZ>B zV{%G%PI?IRwb4#$Ps-%>l6yNC@-P{c3S1czCuL*pWn@iCP07kZYERBihkWUIzOH7= z{9Q3xz$-y1589$XTA>j-Z462)w78c>y&zMwCY{dct;ID7l)Al#0j&l4l}=qbA=I<< zK_AWTQ(bS3Rr{&tekGR2c{Hhi4tflr`)r&xF2~~dOphkbYavCbIZ3CJL$W5NrR1iA zx+b`CB`JaL^=Oj+V}dpfnJGCT7|D#=kg3|6lgiBH|^j`h7q`MD2W$PYL zU(iZWN-B+}>;k1_^&a%pfiWAHgWOzK zdiH;9c$_QOMbn=-HD`Pts4;8sSIytrPAv}~Cu{PAl=0~yWAa_ux1peX8#!2W-~?zR z$Vs3yFPBm71$qLM^6uUcEwTwI8LqUHoZS4On*0SQEyv@aw7jxGY5tKO>PW;6*Yd^4 zZ*>9fIADHT-Z8p%g}kG*?hLu!3rEw^&bIm~n_tV^YZ-jCoZIVXdU(fvlWbWW{1tzv z{^({I=Pz}&oxOeZZO`d@%Elf)ziZZucbcB6UBQ2!dbe}MchmXkx}!g8;L*@D6uVj% zZfO;1G9es=ICFn+4)|1!R|eS33m{>d-7@!TW;1^SsVgLjdwJW;O)=)-YFT0vn<)uW zG_@cv^W~mx9P&@TJg$wy`~qeyOtnIL@^n95-o{}%0A-Zg@o(TFRj!M_t}_>b!vfP> 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 Date: Wed, 4 Mar 2026 15:41:01 -0600 Subject: [PATCH 126/204] used prettier to format everything --- app/client/docker-compose.yml | 4 +- app/client/eslint.config.js | 18 +- app/client/src/App.css | 22 +- app/client/src/App.tsx | 3 +- app/client/src/api/Account.ts | 181 ++++---- app/client/src/api/Income.ts | 203 ++++----- app/client/src/api/ManagerAPI.tsx | 395 +++++++++--------- app/client/src/api/Transaction.ts | 192 ++++----- app/client/src/components/AccountCard.tsx | 11 +- app/client/src/components/AccountForm.tsx | 348 ++++++++------- app/client/src/components/AccountList.tsx | 112 ++--- app/client/src/components/AccountListCard.tsx | 92 ++-- .../src/components/DashboardChartSection.tsx | 285 +++++++------ app/client/src/components/Header.tsx | 29 +- app/client/src/components/IncomeCard.tsx | 94 ++--- app/client/src/components/IncomeForm.tsx | 189 +++++---- app/client/src/components/IncomeList.tsx | 110 ++--- app/client/src/components/Layout.tsx | 8 +- app/client/src/components/LoadingSpinner.tsx | 4 +- app/client/src/components/NavBar.tsx | 10 +- app/client/src/components/SankeyChart.tsx | 39 +- app/client/src/components/TransactionCard.tsx | 99 ++--- app/client/src/components/TransactionList.tsx | 123 +++--- .../src/components/TransactionTable.tsx | 25 +- app/client/src/components/TransationForm.tsx | 331 ++++++++------- .../src/components/csvread/CsvConfirm.tsx | 15 +- app/client/src/components/csvread/CsvDrop.tsx | 4 +- .../src/components/csvread/CsvError.tsx | 3 +- app/client/src/components/csvread/CsvFile.tsx | 6 +- .../src/components/csvread/CsvPreview.tsx | 12 +- .../src/components/csvread/CsvUpload.tsx | 203 +++++---- .../src/components/csvread/csvSuccess.tsx | 6 +- app/client/src/components/userForm.css | 93 ++--- app/client/src/enum/AccountCategory.ts | 10 +- app/client/src/enum/TransactionCategory.ts | 14 +- app/client/src/index.css | 6 +- app/client/src/main.tsx | 12 +- app/client/src/pages/DashboardPage.tsx | 82 +++- app/client/src/types/AccountType.ts | 18 +- app/client/src/types/IncomeType.ts | 9 +- app/client/src/types/Transaction.tsx | 16 +- app/client/src/types/TransactionType.ts | 18 +- app/client/src/types/responseTypes.ts | 6 +- app/client/src/utils/ConvertTransaction.ts | 5 +- app/client/src/utils/NormalizeRow.ts | 30 +- app/client/src/utils/ParseCsv.ts | 17 +- app/client/src/utils/ValidateFile.ts | 10 +- app/client/src/utils/ValidateForms.ts | 84 ++-- app/client/src/utils/constants.ts | 2 +- app/client/src/utils/handleInput.ts | 43 +- app/client/tsconfig.app.json | 4 +- app/client/vite.config.ts | 14 +- app/server/services/ts/README.md | 13 +- .../services/ts/api-gateway/package.json | 1 - .../services/ts/api-gateway/src/index.ts | 42 +- app/server/services/ts/auth/src/logic.ts | 6 +- app/server/services/ts/auth/src/queries.ts | 2 +- .../services/ts/auth/test/logic.test.ts | 163 ++++---- app/server/services/ts/eslint.config.mts | 2 +- app/server/services/ts/tsconfig.json | 6 +- app/server/services/ts/user/package.json | 1 - app/server/services/ts/user/src/index.ts | 26 +- .../services/ts/user/src/routes/account.ts | 13 +- .../services/ts/user/src/routes/profile.ts | 9 +- .../ts/user/src/routes/transaction.ts | 136 +++--- app/server/services/ts/vitest.config.ts | 16 +- 66 files changed, 2231 insertions(+), 1874 deletions(-) diff --git a/app/client/docker-compose.yml b/app/client/docker-compose.yml index 70bb965..92d340d 100644 --- a/app/client/docker-compose.yml +++ b/app/client/docker-compose.yml @@ -3,8 +3,8 @@ services: image: $DOCKER_HUB_USERNAME/finus-webserver:$RELEASE_VERSION build: cache_from: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-webserver:build + - type=registry,ref=$DOCKER_HUB_USERNAME/finus-webserver:build cache_to: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-webserver:build,mode=max + - type=registry,ref=$DOCKER_HUB_USERNAME/finus-webserver:build,mode=max ports: - "80:80" diff --git a/app/client/eslint.config.js b/app/client/eslint.config.js index 5e6b472..75d3c46 100644 --- a/app/client/eslint.config.js +++ b/app/client/eslint.config.js @@ -1,14 +1,14 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ js.configs.recommended, tseslint.configs.recommended, @@ -20,4 +20,4 @@ export default defineConfig([ globals: globals.browser, }, }, -]) +]); diff --git a/app/client/src/App.css b/app/client/src/App.css index eba0feb..b37e0f8 100644 --- a/app/client/src/App.css +++ b/app/client/src/App.css @@ -16,13 +16,23 @@ } .auth-glow-left { - background: radial-gradient(circle, #18cc5f 0%, #0d4d26 70%, transparent 100%); + background: radial-gradient( + circle, + #18cc5f 0%, + #0d4d26 70%, + transparent 100% + ); top: -8rem; left: -8rem; } .auth-glow-right { - background: radial-gradient(circle, #27a552 0%, #0f411d 65%, transparent 100%); + background: radial-gradient( + circle, + #27a552 0%, + #0f411d 65%, + transparent 100% + ); right: -8rem; bottom: -8rem; animation-delay: 1.2s; @@ -140,7 +150,9 @@ color: #deffe9; padding: 0.68rem 0.8rem; outline: none; - transition: border-color 0.2s, box-shadow 0.2s; + transition: + border-color 0.2s, + box-shadow 0.2s; font-size: 0.95rem; } @@ -170,7 +182,9 @@ cursor: pointer; color: #041108; background: linear-gradient(135deg, #53ff8c, #17ac4d); - transition: transform 0.2s, box-shadow 0.2s; + transition: + transform 0.2s, + box-shadow 0.2s; } .auth-button:hover { diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 62d0b84..25ae70f 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -9,7 +9,7 @@ import "./App.css"; import LoginPage from "./pages/LoginPage"; import SignUpPage from "./pages/SignUpPage"; import type { AuthApiResponse, AuthSession, AuthUser } from "./pages/authTypes"; -import DashboardPage from './pages/DashboardPage.tsx' +import DashboardPage from "./pages/DashboardPage.tsx"; const SESSION_STORAGE_KEY = "finus-session"; const API_BASE_URL = @@ -205,5 +205,4 @@ function App() { ); } - export default App; diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index 976486a..b6eae08 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,112 +1,111 @@ import type { updateResponse } from "../types/responseTypes"; import type { Account } from "../types/AccountType"; -const requestUrl = 'http://localhost:3000/api/accounts' +const requestUrl = "http://localhost:3000/api/accounts"; //Sends a request to get different accounts the user has export async function getUserAccounts(): Promise { + try { + //Sends a http request and waits for a response + const response = await fetch(requestUrl, { + method: "GET", + credentials: "include", + }); - try{ - //Sends a http request and waits for a response - const response = await fetch(requestUrl, { - method: 'GET', - credentials: "include" - }) - - //Determine if we were able to retrieve user's data - if(!response.ok) { - - //Failed to retrieve user data, return empty array - console.error("Error: Failed to retrieve users accounts ", response.status) - } - - return response.json() - } catch(error) { - console.error(error) - throw error + //Determine if we were able to retrieve user's data + if (!response.ok) { + //Failed to retrieve user data, return empty array + console.error( + "Error: Failed to retrieve users accounts ", + response.status, + ); } + + return response.json(); + } catch (error) { + console.error(error); + throw error; + } } //Sends a post request to create a user account -export async function postUserAccount(newAccount:Account):Promise { - - try{ - //Create post request and wait for response - const response = await fetch(requestUrl, { - method: "POST", - headers: { - 'content-type': 'application/json' - }, - credentials:"include", - body: JSON.stringify(newAccount) - } ) - - //Determine if our post was a success - if(response.ok){ - alert("Account " + newAccount.name + " has been created" ) - } else { - alert("Failed to create account " + newAccount.name) - console.error(response.status) - } - - return response.json() +export async function postUserAccount( + newAccount: Account, +): Promise { + try { + //Create post request and wait for response + const response = await fetch(requestUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: JSON.stringify(newAccount), + }); - } catch(error) { - console.error(error) - throw error + //Determine if our post was a success + if (response.ok) { + alert("Account " + newAccount.name + " has been created"); + } else { + alert("Failed to create account " + newAccount.name); + console.error(response.status); } + + return response.json(); + } catch (error) { + console.error(error); + throw error; + } } //Updates the users account -export async function putUserAccount(newAccount:Account):Promise { - - try{ - //Create post request and wait for response - const response = await fetch(requestUrl, { - method: "PUT", - headers: { - 'content-type': 'application/json', - }, - credentials: "include", - body: JSON.stringify(newAccount) - } ) - - //Determine if our post was a success - if(response.ok){ - alert("Account " + newAccount.name + " has been updated" ) - } else { - alert("Failed to update account " + newAccount.name) - console.error(response.status) - } - - return response.json() - } catch (error) { - console.error(error) - throw error - } -} +export async function putUserAccount( + newAccount: Account, +): Promise { + try { + //Create post request and wait for response + const response = await fetch(requestUrl, { + method: "PUT", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: JSON.stringify(newAccount), + }); + //Determine if our post was a success + if (response.ok) { + alert("Account " + newAccount.name + " has been updated"); + } else { + alert("Failed to update account " + newAccount.name); + console.error(response.status); + } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} -export async function deleteUserAccount(userAccount:Account) { +export async function deleteUserAccount(userAccount: Account) { + const content = JSON.stringify({ id: userAccount.id }); + //Create delete request to delete the account + const response = await fetch(requestUrl, { + method: "DELETE", + headers: { + "content-type": "/application/json", + }, + credentials: "include", + body: content, + }); - const content = JSON.stringify({id:userAccount.id}) - //Create delete request to delete the account - const response = await fetch(requestUrl,{ - method: "DELETE", - headers: { - "content-type": "/application/json" - }, - credentials:"include", - body: content - }) + if (response.ok) { + alert("Account " + userAccount.name + " has been deleted"); + } else { + alert("Failed to delete account " + userAccount.name); + console.error(response.status); + } - if(response.ok){ - alert("Account " + userAccount.name + " has been deleted") - } else { - alert("Failed to delete account " + userAccount.name) - console.error(response.status) - } - - return response.ok -} \ No newline at end of file + return response.ok; +} diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts index f458294..09a05fe 100644 --- a/app/client/src/api/Income.ts +++ b/app/client/src/api/Income.ts @@ -1,108 +1,117 @@ - -import type { updateResponse } from "../types/responseTypes" -import { type Income } from "../types/IncomeType" - -const requestUrl = "http://localhost:3000/api/income" - -export async function getIncome():Promise { - - try { - const response = await fetch(requestUrl, { - method: "GET", - credentials: "include" - }) - - if(!response.ok) { - console.error(response.status) - alert("Failed to retrieve income") - return [] - } else { - return response.json() - } - } catch(error) { - console.log(error) - throw error +import type { updateResponse } from "../types/responseTypes"; +import { type Income } from "../types/IncomeType"; + +const requestUrl = "http://localhost:3000/api/income"; + +export async function getIncome(): Promise { + try { + const response = await fetch(requestUrl, { + method: "GET", + credentials: "include", + }); + + if (!response.ok) { + console.error(response.status); + alert("Failed to retrieve income"); + return []; + } else { + return response.json(); } + } catch (error) { + console.log(error); + throw error; + } } -export async function postIncome(name:string, income:number, description:string): Promise{ - try{ - const response = await fetch(requestUrl,{ - method: "POST", - headers:{ - "content-type": 'application/json' - }, - credentials: "include", - body: JSON.stringify({"name":name,"income":income,"description":description}) - }) - - if(response.ok){ - alert("Created income " + name) - } else { - alert("Failed to create income " + name) - console.error(response.status) - } - - return response.json() - } catch(error) { - console.error(error) - throw error +export async function postIncome( + name: string, + income: number, + description: string, +): Promise { + try { + const response = await fetch(requestUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ + name: name, + income: income, + description: description, + }), + }); + + if (response.ok) { + alert("Created income " + name); + } else { + alert("Failed to create income " + name); + console.error(response.status); } -} - -export async function putIncome(name:string, income:number, description:string) { - - try { - const response = await fetch(requestUrl,{ - method: "POST", - headers:{ - "content-type": "applicaton/json" - }, - credentials: "include", - body: JSON.stringify({"name":name,"income":income,"description":description}) - }) - - if(response.ok){ - alert("Created income " + name) - } else { - alert("Failed to create income " + name) - console.error(response.status) - } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} - return response.ok - } catch(error) { - console.log(error) - throw error +export async function putIncome( + name: string, + income: number, + description: string, +) { + try { + const response = await fetch(requestUrl, { + method: "POST", + headers: { + "content-type": "applicaton/json", + }, + credentials: "include", + body: JSON.stringify({ + name: name, + income: income, + description: description, + }), + }); + + if (response.ok) { + alert("Created income " + name); + } else { + alert("Failed to create income " + name); + console.error(response.status); } + return response.ok; + } catch (error) { + console.log(error); + throw error; + } } -export async function deleteIncome(selectedIncome:Income) { - - const content = JSON.stringify({id:selectedIncome.id}) - try{ - //Create delete request to delete the income - const response = await fetch(requestUrl,{ - method: "DELETE", - headers: { - "content-type": "/application/json", - }, - credentials:"include", - body: content - }) - - if(response.ok){ - alert("Income " + selectedIncome.name + " has been deleted") - } else { - alert("Failed to delete income " + selectedIncome.name) - console.error(response.status) - } - - return response.ok - - } catch(error) { - console.error(error) - throw error +export async function deleteIncome(selectedIncome: Income) { + const content = JSON.stringify({ id: selectedIncome.id }); + try { + //Create delete request to delete the income + const response = await fetch(requestUrl, { + method: "DELETE", + headers: { + "content-type": "/application/json", + }, + credentials: "include", + body: content, + }); + + if (response.ok) { + alert("Income " + selectedIncome.name + " has been deleted"); + } else { + alert("Failed to delete income " + selectedIncome.name); + console.error(response.status); } -} \ No newline at end of file + + return response.ok; + } catch (error) { + console.error(error); + throw error; + } +} diff --git a/app/client/src/api/ManagerAPI.tsx b/app/client/src/api/ManagerAPI.tsx index ed95b6f..49ab3a2 100644 --- a/app/client/src/api/ManagerAPI.tsx +++ b/app/client/src/api/ManagerAPI.tsx @@ -1,216 +1,235 @@ import axios from "axios"; import { BASE_URL } from "@/utils/constants"; import type { ChartData } from "chart.js"; -import type { SankeyData } from 'recharts/types/chart/Sankey'; +import type { SankeyData } from "recharts/types/chart/Sankey"; import type { Transaction } from "@/types/Transaction"; async function getTransactions(): Promise { - /* Uncomment this block once backend is ready to test fetching transactions */ - // try { - // const response = await axios.get(`${BASE_URL}/api/transactions`); - // if (response.status !== 200) { - // throw new Error(`Failed to fetch transactions: ${response.statusText}`); - // } - // return response.data; - // } catch (error) { - // console.error("Error fetching transactions:", error); - // throw error; - // } + /* Uncomment this block once backend is ready to test fetching transactions */ + // try { + // const response = await axios.get(`${BASE_URL}/api/transactions`); + // if (response.status !== 200) { + // throw new Error(`Failed to fetch transactions: ${response.statusText}`); + // } + // return response.data; + // } catch (error) { + // console.error("Error fetching transactions:", error); + // throw error; + // } - //Assume doing api call here and getting transactions back, but for now just simulate with timeout - const transactions: Transaction[] = [ - { - id: "tx001", - amount: 1200, - category: "Income", - date: "2026-02-01", - from: "Tech Corp Inc.", - to: "Huy Truong", - description: "Monthly salary payment" - }, - { - id: "tx002", - amount: -75.5, - category: "Expenses", - date: "2026-02-02", - from: "Huy Truong", - to: "Walmart", - description: "Weekly grocery shopping" - }, - { - id: "tx003", - amount: -45, - category: "Expenses", - date: "2026-02-03", - from: "Huy Truong", - to: "City Bus Service", - description: "Monthly bus pass" - }, - { - id: "tx004", - amount: -120, - category: "Expenses", - date: "2026-02-04", - from: "Huy Truong", - to: "Hydro Company", - description: "Electricity bill" - }, - { - id: "tx005", - amount: -60, - category: "Expenses", - date: "2026-02-05", - from: "Huy Truong", - to: "Rogers", - description: "Home internet bill" - }, - { - id: "tx006", - amount: -35.75, - category: "Expenses", - date: "2026-02-06", - from: "Huy Truong", - to: "Tim Hortons", - description: "Lunch with friends" - }, - { - id: "tx007", - amount: -15.99, - category: "Expenses", - date: "2026-02-07", - from: "Huy Truong", - to: "Netflix", - description: "Monthly subscription" - }, - { - id: "tx008", - amount: -200, - category: "Expenses", - date: "2026-02-08", - from: "Huy Truong", - to: "Amazon", - description: "Electronics purchase" - }, - { - id: "tx009", - amount: 150, - category: "Expenses", - date: "2026-02-09", - from: "Client ABC", - to: "Huy Truong", - description: "Website development payment" - }, - { - id: "tx010", - amount: -500, - category: "Expenses", - date: "2026-02-01", - from: "Huy Truong", - to: "Landlord", - description: "Monthly rent" - } - ]; - return new Promise(resolve => setTimeout(() => resolve(transactions), 1000)); + //Assume doing api call here and getting transactions back, but for now just simulate with timeout + const transactions: Transaction[] = [ + { + id: "tx001", + amount: 1200, + category: "Income", + date: "2026-02-01", + from: "Tech Corp Inc.", + to: "Huy Truong", + description: "Monthly salary payment", + }, + { + id: "tx002", + amount: -75.5, + category: "Expenses", + date: "2026-02-02", + from: "Huy Truong", + to: "Walmart", + description: "Weekly grocery shopping", + }, + { + id: "tx003", + amount: -45, + category: "Expenses", + date: "2026-02-03", + from: "Huy Truong", + to: "City Bus Service", + description: "Monthly bus pass", + }, + { + id: "tx004", + amount: -120, + category: "Expenses", + date: "2026-02-04", + from: "Huy Truong", + to: "Hydro Company", + description: "Electricity bill", + }, + { + id: "tx005", + amount: -60, + category: "Expenses", + date: "2026-02-05", + from: "Huy Truong", + to: "Rogers", + description: "Home internet bill", + }, + { + id: "tx006", + amount: -35.75, + category: "Expenses", + date: "2026-02-06", + from: "Huy Truong", + to: "Tim Hortons", + description: "Lunch with friends", + }, + { + id: "tx007", + amount: -15.99, + category: "Expenses", + date: "2026-02-07", + from: "Huy Truong", + to: "Netflix", + description: "Monthly subscription", + }, + { + id: "tx008", + amount: -200, + category: "Expenses", + date: "2026-02-08", + from: "Huy Truong", + to: "Amazon", + description: "Electronics purchase", + }, + { + id: "tx009", + amount: 150, + category: "Expenses", + date: "2026-02-09", + from: "Client ABC", + to: "Huy Truong", + description: "Website development payment", + }, + { + id: "tx010", + amount: -500, + category: "Expenses", + date: "2026-02-01", + from: "Huy Truong", + to: "Landlord", + description: "Monthly rent", + }, + ]; + return new Promise((resolve) => + setTimeout(() => resolve(transactions), 1000), + ); } - - //API for calling for various charts below // Expenses over time chart // Accepted time periods are "w" for weekly, "m" for monthly, and "y" for yearly - the backend will handle the units async function getExpensesChartData(period: string): Promise> { - try { - if (!["w", "m", "y"].includes(period)) { - throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); - } - const response = await axios.get(`${BASE_URL}/charts/expenses?period=${period}`); - if (response.status !== 200) { - throw new Error(`Failed to fetch expenses chart data: ${response.statusText}`); - } - - return { - labels: response.data["labels"], - datasets: [{ - label: response.data["datasets"][0]["label"], - data: response.data["datasets"][0]["data"], - borderColor: 'rgb(254, 103, 48)', - backgroundColor: 'rgba(255, 47, 47, 0.5)', - }] - }; + try { + if (!["w", "m", "y"].includes(period)) { + throw new Error( + "Invalid period. Must be 'weekly', 'monthly', or 'yearly'.", + ); + } + const response = await axios.get( + `${BASE_URL}/charts/expenses?period=${period}`, + ); + if (response.status !== 200) { + throw new Error( + `Failed to fetch expenses chart data: ${response.statusText}`, + ); + } - } catch (error) { - console.error("Error fetching expenses chart data:", error); - throw error; + return { + labels: response.data["labels"], + datasets: [ + { + label: response.data["datasets"][0]["label"], + data: response.data["datasets"][0]["data"], + borderColor: "rgb(254, 103, 48)", + backgroundColor: "rgba(255, 47, 47, 0.5)", + }, + ], }; + } catch (error) { + console.error("Error fetching expenses chart data:", error); + throw error; + } } - // Savings contribution chart -async function getSavingsContribChartData(period: string): Promise> { - try { - if (!["w", "m", "y"].includes(period)) { - throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); - } - const response = await axios.get(`${BASE_URL}/charts/savings?period=${period}`); - if (response.status !== 200) { - throw new Error(`Failed to fetch savings contribution chart data: ${response.statusText}`); - } - console.log("Received savings contribution chart data:", response.data); - return { - labels: response.data["labels"], - datasets: [{ - label: response.data["datasets"][0]["label"], - data: response.data["datasets"][0]["data"], - borderColor: 'rgb(68, 255, 21)', - backgroundColor: 'rgba(68, 255, 21, 0.5)', - }] - }; - - } catch (error) { - console.error("Error fetching savings contribution chart data:", error); - throw error; +async function getSavingsContribChartData( + period: string, +): Promise> { + try { + if (!["w", "m", "y"].includes(period)) { + throw new Error( + "Invalid period. Must be 'weekly', 'monthly', or 'yearly'.", + ); + } + const response = await axios.get( + `${BASE_URL}/charts/savings?period=${period}`, + ); + if (response.status !== 200) { + throw new Error( + `Failed to fetch savings contribution chart data: ${response.statusText}`, + ); + } + console.log("Received savings contribution chart data:", response.data); + return { + labels: response.data["labels"], + datasets: [ + { + label: response.data["datasets"][0]["label"], + data: response.data["datasets"][0]["data"], + borderColor: "rgb(68, 255, 21)", + backgroundColor: "rgba(68, 255, 21, 0.5)", + }, + ], }; + } catch (error) { + console.error("Error fetching savings contribution chart data:", error); + throw error; + } } - // Income flow - sankey chart data async function getIncomeFlowChartData(period: string): Promise { - try { - if (!["w", "m", "y"].includes(period)) { - throw new Error("Invalid period. Must be 'weekly', 'monthly', or 'yearly'."); - } - const response = await axios.get(`${BASE_URL}/charts/incomeflow?period=${period}`); - if (response.status !== 200) { - throw new Error(`Failed to fetch income flow chart data: ${response.statusText}`); - } - return response.data; - // const dataTest = { - // nodes: [ - // { name: 'Salary' }, - // { name: 'Pokemon Cards' }, - // { name: 'Total Income' }, - // { name: 'Food' }, - // { name: 'Mortgage' }, - // ], - // links: [ - // { source: 0, target: 1, value: 3728.3 }, - // { source: 0, target: 2, value: 354170 }, - // { source: 2, target: 3, value: 62429 }, - // { source: 2, target: 4, value: 291741 }, - // ], - // }; - // return dataTest; - } catch (error) { - console.error("Error fetching income flow chart data:", error); - throw error; - }; + try { + if (!["w", "m", "y"].includes(period)) { + throw new Error( + "Invalid period. Must be 'weekly', 'monthly', or 'yearly'.", + ); + } + const response = await axios.get( + `${BASE_URL}/charts/incomeflow?period=${period}`, + ); + if (response.status !== 200) { + throw new Error( + `Failed to fetch income flow chart data: ${response.statusText}`, + ); + } + return response.data; + // const dataTest = { + // nodes: [ + // { name: 'Salary' }, + // { name: 'Pokemon Cards' }, + // { name: 'Total Income' }, + // { name: 'Food' }, + // { name: 'Mortgage' }, + // ], + // links: [ + // { source: 0, target: 1, value: 3728.3 }, + // { source: 0, target: 2, value: 354170 }, + // { source: 2, target: 3, value: 62429 }, + // { source: 2, target: 4, value: 291741 }, + // ], + // }; + // return dataTest; + } catch (error) { + console.error("Error fetching income flow chart data:", error); + throw error; + } } - export { - getTransactions, - getExpensesChartData, - getSavingsContribChartData, - getIncomeFlowChartData + getTransactions, + getExpensesChartData, + getSavingsContribChartData, + getIncomeFlowChartData, }; diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 291e8a9..e0b0418 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,113 +1,113 @@ import type { updateResponse } from "../types/responseTypes"; import { type Transaction } from "../types/TransactionType"; -const requestUrl = "http://localhost:3000/api/transacitons" +const requestUrl = "http://localhost:3000/api/transacitons"; //Sends a GET request to get the list of user transactions for the account -export async function getTransactions(account_id: number): Promise{ - - try { - //Put as object to convert to json when sent in the body - const content = {"id": account_id} - - const response = await fetch(requestUrl, { - method: "GET", - credentials: "include", - body: JSON.stringify(content) - }) - - if(!response.ok){ - - alert("Failed to retrieve account's transaction\n") - console.error(response.status) - } - - return response.json() - } catch(error){ - console.error(error) - throw error +export async function getTransactions( + account_id: number, +): Promise { + try { + //Put as object to convert to json when sent in the body + const content = { id: account_id }; + + const response = await fetch(requestUrl, { + method: "GET", + credentials: "include", + body: JSON.stringify(content), + }); + + if (!response.ok) { + alert("Failed to retrieve account's transaction\n"); + console.error(response.status); } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } } //Can send multiple transactions in a push request -export async function postTranscations(trans:Transaction[]):Promise{ - - try { - const response = await fetch(requestUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - credentials:"include", - body: JSON.stringify(trans) - }) - - if(response.ok){ - alert("Create the transaction") - } else { - alert("Failed to create transaction") - console.error(response.status) - } - - return response.json() - } catch(error) { - console.log(error) - throw(error) +export async function postTranscations( + trans: Transaction[], +): Promise { + try { + const response = await fetch(requestUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: JSON.stringify(trans), + }); + + if (response.ok) { + alert("Create the transaction"); + } else { + alert("Failed to create transaction"); + console.error(response.status); } -} -export async function putTranscations(trans:Transaction):Promise{ - - try { - const response = await fetch(requestUrl, { - method: "PUT", - headers: { - "content-type": "application/json", - }, - credentials: "include", - body: JSON.stringify(trans) - }) - - if(response.ok){ - alert("Updated the transaction") - } else { - alert("Failed to update transaction") - console.error(response.status) - } - - return response.ok - - } catch(error){ - console.log(error) - throw error - } + return response.json(); + } catch (error) { + console.log(error); + throw error; + } } -export async function deleteTransaction(selectedTransaction:Transaction) { - try { - const content = JSON.stringify({id:selectedTransaction.id, financialAccount_id:selectedTransaction.financialAccount_id}) - //Create delete request to delete the account - const response = await fetch(requestUrl,{ - method: "DELETE", - headers: { - "content-type": "application/json", - }, - credentials: "include", - body: content - }) +export async function putTranscations(trans: Transaction): Promise { + try { + const response = await fetch(requestUrl, { + method: "PUT", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: JSON.stringify(trans), + }); + + if (response.ok) { + alert("Updated the transaction"); + } else { + alert("Failed to update transaction"); + console.error(response.status); + } - if(response.ok){ - alert("Transaction has been deleted") - } else { - alert("Failed to delete transaction") - console.error(response.status) - } - - return response.ok + return response.ok; + } catch (error) { + console.log(error); + throw error; + } +} - } catch(error) { - console.log(error) - throw error +export async function deleteTransaction(selectedTransaction: Transaction) { + try { + const content = JSON.stringify({ + id: selectedTransaction.id, + financialAccount_id: selectedTransaction.financialAccount_id, + }); + //Create delete request to delete the account + const response = await fetch(requestUrl, { + method: "DELETE", + headers: { + "content-type": "application/json", + }, + credentials: "include", + body: content, + }); + + if (response.ok) { + alert("Transaction has been deleted"); + } else { + alert("Failed to delete transaction"); + console.error(response.status); } -} \ No newline at end of file + + return response.ok; + } catch (error) { + console.log(error); + throw error; + } +} diff --git a/app/client/src/components/AccountCard.tsx b/app/client/src/components/AccountCard.tsx index 56a9db1..9e27ad2 100644 --- a/app/client/src/components/AccountCard.tsx +++ b/app/client/src/components/AccountCard.tsx @@ -2,15 +2,18 @@ type AccountCardProps = { title: string; amount: string; backgroundColor: string | "#2a2a2a"; -} +}; function AccountCard(props: AccountCardProps) { const { title, amount, backgroundColor } = props; return ( -
+

{title}

{amount}

- ) + ); } -export default AccountCard \ No newline at end of file +export default AccountCard; diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index f8071c1..222f9c1 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -1,99 +1,124 @@ import React, { useState } from "react"; -import './userForm.css' -import {postUserAccount, putUserAccount} from '../api/Account.ts' +import "./userForm.css"; +import { postUserAccount, putUserAccount } from "../api/Account.ts"; import { type Account } from "../types/AccountType.ts"; import { validateAccountForm } from "../utils/ValidateForms.ts"; -import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput.ts"; +import { + handleCurrencyChange, + handleCurrencyBlur, +} from "../utils/handleInput.ts"; import { accountCategory } from "@/enum/AccountCategory.ts"; -interface popupProp{ - toggle: () => void; - setAccount?: (account:Account) => void - addAccount?: (account:Account) => void - edit: boolean - selectedAccount?: Account +interface popupProp { + toggle: () => void; + setAccount?: (account: Account) => void; + addAccount?: (account: Account) => void; + edit: boolean; + selectedAccount?: Account; } -type typeofAccount = keyof typeof accountCategory +type typeofAccount = keyof typeof accountCategory; -export default function PopupForm({toggle, setAccount, addAccount, edit, selectedAccount}:popupProp){ +export default function PopupForm({ + toggle, + setAccount, + addAccount, + edit, + selectedAccount, +}: popupProp) { + //Submits the account data - //Submits the account data - - const handleSubmit = () => { - let subtype = undefined - let interest = undefined - const accountBalance = Number(balance) + const handleSubmit = () => { + let subtype = undefined; + let interest = undefined; + const accountBalance = Number(balance); - if(accountType === accountCategory.SAVING) { - subtype = formInput.subType - } + if (accountType === accountCategory.SAVING) { + subtype = formInput.subType; + } + + if ( + accountType === accountCategory.SAVING || + accountType === accountCategory.DEBT + ) { + interest = formInput.interest; + } - if(accountType === accountCategory.SAVING || accountType === accountCategory.DEBT){ - interest = formInput.interest + //Check account before validating b/c account can be undefined + //Determine if form input for an account is valid + if ( + accountType && + validateAccountForm( + formInput.name, + accountCategory[accountType], + accountBalance, + undefined, + subtype, + interest, + ) + ) { + const newAccount: Account = { + id: 0, + name: formInput.name, + type: accountType, + balance: accountBalance, + subtype: "", + value: 0, + last_updated: new Date(), + }; + console.log(newAccount); + + //Determine if we edding an account info + if (edit && selectedAccount) { + newAccount.id = selectedAccount.id; + + try { + //Put reques to update the account + putUserAccount(newAccount).then((response) => { + //Determine if sucessfully post + if (response && response.lastUpdated && addAccount) { + newAccount.last_updated = response.lastUpdated; + addAccount(newAccount); + } + }); + } catch { + alert("Failed to update Account"); } - - //Check account before validating b/c account can be undefined - //Determine if form input for an account is valid - if(accountType && validateAccountForm(formInput.name, accountCategory[accountType], accountBalance,undefined,subtype,interest)) { - - const newAccount: Account = {id: 0, name:formInput.name, type: accountType, balance: accountBalance, subtype:"", value:0, last_updated: new Date()} - console.log(newAccount) - - //Determine if we edding an account info - if(edit && selectedAccount) { - newAccount.id = selectedAccount.id - - try{ - //Put reques to update the account - putUserAccount(newAccount).then((response) =>{ - - //Determine if sucessfully post - if(response && response.lastUpdated && addAccount) { - newAccount.last_updated = response.lastUpdated - addAccount(newAccount) - } - }) - } catch{ - alert("Failed to update Account") - } - - } else { - try{ - //Send a post request to create or edit the account - postUserAccount(newAccount).then((response)=>{ - - //When creating new account, id and lastupdated is returned for that account - if(response && response.id) - newAccount.id = response.id - - if(response.lastUpdated){ - newAccount.last_updated = response.lastUpdated - - //Check if setAccount is undefined - if(setAccount){ - setAccount(newAccount) - } - } - }) - } catch{ - alert("Failed to create account " + newAccount.name) - } + } else { + try { + //Send a post request to create or edit the account + postUserAccount(newAccount).then((response) => { + //When creating new account, id and lastupdated is returned for that account + if (response && response.id) newAccount.id = response.id; + + if (response.lastUpdated) { + newAccount.last_updated = response.lastUpdated; + + //Check if setAccount is undefined + if (setAccount) { + setAccount(newAccount); + } } - - toggle() - } else { - alert("Please fill out the form") + }); + } catch { + alert("Failed to create account " + newAccount.name); } - } - + } - const handleChange = (event:React.ChangeEvent) => { - setFormInput({...formInput, [event.target.name] : event.target.value}); + toggle(); + } else { + alert("Please fill out the form"); } + }; + + const handleChange = ( + event: React.ChangeEvent, + ) => { + setFormInput({ ...formInput, [event.target.name]: event.target.value }); + }; - //Handles the bank statement - /*const handleFile = (event:React.ChangeEvent) => { + //Handles the bank statement + /*const handleFile = (event:React.ChangeEvent) => { if(event.target && event.target.files && event.target.files[0]){ setFile(event.target.files[0]) @@ -101,83 +126,118 @@ export default function PopupForm({toggle, setAccount, addAccount, edit, selecte setFile(undefined) } }*/ - - - const [formInput, setFormInput] = useState({name: '', subType:'', interest: 0}) - //const [file, setFile] = useState(undefined) - const[balance, setBalance] = useState("") - const[accountType, setAccountType] = useState(undefined) - - const accountCat: typeofAccount[] = Object.keys(accountCategory) as typeofAccount[] - - //Determine if we're editting an account - if(edit && selectedAccount) { - setFormInput({...formInput,["name"]:selectedAccount.name}) - - if(selectedAccount.subtype){ - setFormInput({...formInput, ["subType"]: selectedAccount.subtype}) - } - setBalance(selectedAccount.balance.toString()) - setAccountType(selectedAccount.type as typeofAccount) + const [formInput, setFormInput] = useState({ + name: "", + subType: "", + interest: 0, + }); + //const [file, setFile] = useState(undefined) + const [balance, setBalance] = useState(""); + const [accountType, setAccountType] = useState( + undefined, + ); + + const accountCat: typeofAccount[] = Object.keys( + accountCategory, + ) as typeofAccount[]; + + //Determine if we're editting an account + if (edit && selectedAccount) { + setFormInput({ ...formInput, ["name"]: selectedAccount.name }); + + if (selectedAccount.subtype) { + setFormInput({ ...formInput, ["subType"]: selectedAccount.subtype }); } + setBalance(selectedAccount.balance.toString()); + setAccountType(selectedAccount.type as typeofAccount); + } - return( + return ( <>
-
- - {edit ? (

Edit Account

):(

Create Account

)} - - - -

- - - -

- - - handleCurrencyChange(event, setBalance)} onBlur ={(event) => handleCurrencyBlur(event,balance,setBalance)} placeholder="0.00"/> -

- - - {(accountType === "DEBT" || accountType === "SAVING") ? ( - <> - - -

- - ): null + {edit ?

Edit Account

:

Create Account

} + + + +

+ + + +

+ + + handleCurrencyChange(event, setBalance)} + onBlur={(event) => handleCurrencyBlur(event, balance, setBalance)} + placeholder="0.00" + /> +

+ + {accountType === "DEBT" || accountType === "SAVING" ? ( + <> + + +

+ + ) : null} - {(accountType === "SAVING") ? ( - <> + {accountType === "SAVING" ? ( + <> - + + + +

- - ):null - } - -
- - {edit? (): ()} -
+ + ) : null} + +
+ + {edit ? ( + + ) : ( + + )} +
-
- - ) + + ); } - diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx index 15ad7a1..0bb34e8 100644 --- a/app/client/src/components/AccountList.tsx +++ b/app/client/src/components/AccountList.tsx @@ -1,56 +1,64 @@ -import { useState } from "react" -import { getUserAccounts} from "../api/Account" -import { type Account } from "../types/AccountType" -import AccountListCard from "./AccountListCard" -import AccountPopup from "./AccountForm" - - -export default function AccountList(){ - - //Stores a lits of the user's account - const [userAccounts, setUserAccounts] = useState([]) - - const [seen, setSeen] = useState(false) - - //Try to get the accounts from the server - try { - getUserAccounts().then((accounts)=>{ - //Determine if we acquired the accounts - if(accounts) { - setUserAccounts(accounts) - } - }) - } catch { - alert("Failed to retrieve user accounts") - } - - //Adds a account to the list - const addAccount = (newAccount:Account) => { - setUserAccounts(userAccounts => [...userAccounts,newAccount]) - } - - //Removes any account from the list thats shares the same id - const removeAccount = (remove:Account) => { - setUserAccounts(userAccounts=> userAccounts.filter((account) => account.id !== remove.id)) - } - - const toggle = () => { - setSeen(!seen) - } - - return( - <> +import { useState } from "react"; +import { getUserAccounts } from "../api/Account"; +import { type Account } from "../types/AccountType"; +import AccountListCard from "./AccountListCard"; +import AccountPopup from "./AccountForm"; + +export default function AccountList() { + //Stores a lits of the user's account + const [userAccounts, setUserAccounts] = useState([]); + + const [seen, setSeen] = useState(false); + + //Try to get the accounts from the server + try { + getUserAccounts().then((accounts) => { + //Determine if we acquired the accounts + if (accounts) { + setUserAccounts(accounts); + } + }); + } catch { + alert("Failed to retrieve user accounts"); + } + + //Adds a account to the list + const addAccount = (newAccount: Account) => { + setUserAccounts((userAccounts) => [...userAccounts, newAccount]); + }; + + //Removes any account from the list thats shares the same id + const removeAccount = (remove: Account) => { + setUserAccounts((userAccounts) => + userAccounts.filter((account) => account.id !== remove.id), + ); + }; + + const toggle = () => { + setSeen(!seen); + }; + + return ( + <> +
-
- {userAccounts.map(account => ())} -
+ {userAccounts.map((account) => ( + + ))} +
-
- -
+
+
+
- {seen ? ():null} - - ) -} \ No newline at end of file + {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx index f7ff325..e44afbe 100644 --- a/app/client/src/components/AccountListCard.tsx +++ b/app/client/src/components/AccountListCard.tsx @@ -1,60 +1,56 @@ import { deleteUserAccount } from "../api/Account.ts"; import { useState } from "react"; -import AccountPopup from './AccountForm.tsx' +import AccountPopup from "./AccountForm.tsx"; import { type Account } from "../types/AccountType.ts"; -interface cardProp{ - account:Account - setAccount: (editAccount:Account) => void - removeAccount: (removeAccount:Account) => void +interface cardProp { + account: Account; + setAccount: (editAccount: Account) => void; + removeAccount: (removeAccount: Account) => void; } - - -export default function Card({account, setAccount, removeAccount}:cardProp) { - - //Stores the value that toggles thhe account popup form - const [seen, setSeen] = useState(false) - - const toggle = () => { - setSeen(!seen) - } - - //Deletes the user account from the server and then remove it from the list - const deleteAccount = () => { - - try { - //Send a request to delete user account - deleteUserAccount(account).then((result)=>{ - - //Sucessfully deleted account - if(result) { - removeAccount(account) - } - }) - } catch{ - alert("Failed to delete account " + account.name) +export default function Card({ account, setAccount, removeAccount }: cardProp) { + //Stores the value that toggles thhe account popup form + const [seen, setSeen] = useState(false); + + const toggle = () => { + setSeen(!seen); + }; + + //Deletes the user account from the server and then remove it from the list + const deleteAccount = () => { + try { + //Send a request to delete user account + deleteUserAccount(account).then((result) => { + //Sucessfully deleted account + if (result) { + removeAccount(account); } + }); + } catch { + alert("Failed to delete account " + account.name); } + }; - return( - <> -
-

{account.name}

-

- -
-

{account.type + " " + account.subtype}

-

{account.balance}

-
+ return ( + <> +
+

{account.name}

+

-
- - -
+
+

{account.type + " " + account.subtype}

+

{account.balance}

- {seen ? ():null} - - ) -} \ No newline at end of file +
+ + +
+
+ {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/DashboardChartSection.tsx b/app/client/src/components/DashboardChartSection.tsx index e8fd81a..55af367 100644 --- a/app/client/src/components/DashboardChartSection.tsx +++ b/app/client/src/components/DashboardChartSection.tsx @@ -1,25 +1,34 @@ -import { useEffect, useState } from 'react' -import { Line, Bar } from 'react-chartjs-2'; -import LoadingSpinner from '@/components/LoadingSpinner'; -import SankeyChart from '@/components/SankeyChart'; -import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData } from '@/api/ManagerAPI'; -import type { SankeyData } from 'recharts/types/chart/Sankey'; -import type { ChartData } from 'chart.js'; +import { useEffect, useState } from "react"; +import { Line, Bar } from "react-chartjs-2"; +import LoadingSpinner from "@/components/LoadingSpinner"; +import SankeyChart from "@/components/SankeyChart"; +import { + getExpensesChartData, + getSavingsContribChartData, + getIncomeFlowChartData, +} from "@/api/ManagerAPI"; +import type { SankeyData } from "recharts/types/chart/Sankey"; +import type { ChartData } from "chart.js"; function DashboardChartSection() { - //Active chart state - this just determines which chart is displayed in the holder - change this later to potentially load up all charts at once if latency is good - const [activeChart, setActiveChart] = useState<'expenses' | 'savings' | 'income'>('expenses'); - - //Active chart period state - this determines over what timeframe the chart is displayed - weekly, monthly, or yearly - default to monthly - const [selectedPeriod, setSelectedPeriod] = useState<'w' | 'm' | 'y'>('m'); - - //Chart data state - this is used to determine whether to put a spinner in place of a chart while data is being fetched from API - const [expensesData, setExpensesData] = useState | null>(null); - const [savingsData, setsavingsData] = useState | null>(null); - const [incomeData, setIncomeData] = useState(null); - const [isLoading, setIsLoading] = useState(false); + //Active chart state - this just determines which chart is displayed in the holder - change this later to potentially load up all charts at once if latency is good + const [activeChart, setActiveChart] = useState< + "expenses" | "savings" | "income" + >("expenses"); - - async function fetchExpensesData() { + //Active chart period state - this determines over what timeframe the chart is displayed - weekly, monthly, or yearly - default to monthly + const [selectedPeriod, setSelectedPeriod] = useState<"w" | "m" | "y">("m"); + + //Chart data state - this is used to determine whether to put a spinner in place of a chart while data is being fetched from API + const [expensesData, setExpensesData] = useState | null>( + null, + ); + const [savingsData, setsavingsData] = useState | null>( + null, + ); + const [incomeData, setIncomeData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + async function fetchExpensesData() { setIsLoading(true); try { const data = await getTestExpensesData(selectedPeriod); @@ -29,7 +38,7 @@ function DashboardChartSection() { } finally { setIsLoading(false); } - }; + } async function fetchSavingsData() { setIsLoading(true); @@ -41,7 +50,7 @@ function DashboardChartSection() { } finally { setIsLoading(false); } - }; + } async function fetchIncomeData() { setIsLoading(true); @@ -54,163 +63,160 @@ function DashboardChartSection() { } finally { setIsLoading(false); } - }; - + } useEffect(() => { - switch(activeChart) { - case 'expenses': + switch (activeChart) { + case "expenses": fetchExpensesData(); break; - case 'savings': + case "savings": fetchSavingsData(); break; - case 'income': + case "income": fetchIncomeData(); break; default: - fetchExpensesData(); - } + fetchExpensesData(); + } }, [activeChart, selectedPeriod]); - //Temporary test data for expenses - delete once API works. This is just to test graph components - const getTestExpensesData = async (period: 'w' | 'm' | 'y'): Promise> => { + const getTestExpensesData = async ( + period: "w" | "m" | "y", + ): Promise> => { //Try to reach API first, get synthetic data if fails - try{ + try { const response = await getExpensesChartData(period); return response; - }catch(error) { + } catch (error) { console.error("Error fetching expenses chart data:", error); } return { - labels: ['Mon'], - datasets: [{ - label: 'Placeholder Expenses', - data: [125], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }] + labels: ["Mon"], + datasets: [ + { + label: "Placeholder Expenses", + data: [125], + borderColor: "rgb(53, 162, 235)", + backgroundColor: "rgba(53, 162, 235, 0.5)", + }, + ], }; }; - - const getTestSavingsContribData = async (period: 'w' | 'm' | 'y'): Promise> => { - try{ + const getTestSavingsContribData = async ( + period: "w" | "m" | "y", + ): Promise> => { + try { const response = await getSavingsContribChartData(period); return response; - }catch(error) { + } catch (error) { console.error("Error fetching savings contribution chart data:", error); } return { - labels: ['Mon'], - datasets: [{ - label: 'Placeholder Savings Contribution', - data: [125], - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }] + labels: ["Mon"], + datasets: [ + { + label: "Placeholder Savings Contribution", + data: [125], + borderColor: "rgb(53, 162, 235)", + backgroundColor: "rgba(53, 162, 235, 0.5)", + }, + ], }; - } + }; - const getTestIncomeFlowData = async (period: 'w' | 'm' | 'y'): Promise => { - try{ + const getTestIncomeFlowData = async ( + period: "w" | "m" | "y", + ): Promise => { + try { const response = await getIncomeFlowChartData(period); //console.log("Received income flow chart data:", response); return response; - }catch(error) { + } catch (error) { console.error("Error fetching income flow chart data:", error); } //console.log("Using placeholder income flow chart data"); return { nodes: [], links: [], - }; - } - + }; + }; -const expensesBarOptions = { - responsive: true, - plugins: { - legend: { - position: 'top' as const, + const expensesBarOptions = { + responsive: true, + plugins: { + legend: { + position: "top" as const, + }, + title: { + display: true, + text: "Expenses Over Time", + font: { + size: 24, + weight: "bold" as const, + }, + }, }, - title: { - display: true, - text: 'Expenses Over Time', - font: { - size: 24, - weight: 'bold' as const - } - } - }, -}; + }; -const savingsLineOptions = { - responsive: true, - plugins: { - legend: { - position: 'top' as const, + const savingsLineOptions = { + responsive: true, + plugins: { + legend: { + position: "top" as const, + }, + title: { + display: true, + text: "Expenses Over Time", + font: { + size: 24, + weight: "bold" as const, + }, + }, }, - title: { - display: true, - text: 'Expenses Over Time', - font: { - size: 24, - weight: 'bold' as const - } - } - }, -}; - + }; -//Renders a chart based on activeChart - puts a spinner in place while data is being fetched or if no data is available for any reason + //Renders a chart based on activeChart - puts a spinner in place while data is being fetched or if no data is available for any reason const renderChart = () => { - switch(activeChart) { - case 'expenses': + switch (activeChart) { + case "expenses": if (isLoading || !expensesData) { return (
- < LoadingSpinner /> +
); } - return ( - - - - ); - case 'savings': + return ; + case "savings": if (isLoading || !savingsData) { return (
- < LoadingSpinner /> +
); } - return ( - - - - ); - case 'income': + return ; + case "income": if (isLoading || !incomeData) { return (
- < LoadingSpinner /> +
); } return ( //
- + //
); default: return (
{/* figure out a default case in case of an error - can have a placeholder or a spinner chart (loading spinner*/} - < LoadingSpinner /> +
); } @@ -218,17 +224,59 @@ const savingsLineOptions = { return ( <>
- - - + + +
{/* This chart is here just to test all the graph components */}
- - - + + +
{renderChart()} {/*
@@ -237,8 +285,7 @@ const savingsLineOptions = {
- ) + ); } - -export default DashboardChartSection +export default DashboardChartSection; diff --git a/app/client/src/components/Header.tsx b/app/client/src/components/Header.tsx index 66201ad..5b987ca 100644 --- a/app/client/src/components/Header.tsx +++ b/app/client/src/components/Header.tsx @@ -1,25 +1,28 @@ -import {useState} from 'react' +import { useState } from "react"; import { VscThreeBars } from "react-icons/vsc"; import { IoPersonCircleSharp } from "react-icons/io5"; -import NavBar from './NavBar'; +import NavBar from "./NavBar"; function Header() { - const [isSidebarOpen, setIsSidebarOpen] = useState(false) + const [isSidebarOpen, setIsSidebarOpen] = useState(false); const toggleSidebar = () => { - setIsSidebarOpen(prev => !prev) - } - + setIsSidebarOpen((prev) => !prev); + }; + return ( <> {isSidebarOpen && } -
- -
-

Dashboard

+
+ +
+

Dashboard

- +
- ) + ); } -export default Header \ No newline at end of file +export default Header; diff --git a/app/client/src/components/IncomeCard.tsx b/app/client/src/components/IncomeCard.tsx index 0657935..3aa3a3e 100644 --- a/app/client/src/components/IncomeCard.tsx +++ b/app/client/src/components/IncomeCard.tsx @@ -1,57 +1,55 @@ -import { useState } from "react" -import { deleteIncome} from "../api/Income" -import { type Income } from "../types/IncomeType" -import IncomePopup from "./IncomeForm" - -interface cardProp{ - income: Income - setIncome: (editIncome:Income) => void - removeIncome: (removeIncome:Income) => void +import { useState } from "react"; +import { deleteIncome } from "../api/Income"; +import { type Income } from "../types/IncomeType"; +import IncomePopup from "./IncomeForm"; + +interface cardProp { + income: Income; + setIncome: (editIncome: Income) => void; + removeIncome: (removeIncome: Income) => void; } -export default function Card({income, setIncome, removeIncome}:cardProp) { - - const [seen, setSeen] = useState(false) - - const toggle = () => { - setSeen(!seen) - } - - //Deletes the user income from the server and then remove it from the list - const deleteUserIncome = () => { - - try { - //Send a request to delete user's income - deleteIncome(income).then((result)=>{ - - //Sucessfully deleted income - if(result) { - removeIncome(income) - } - }) - } catch { - alert("Failed to delete income " + income.name) +export default function Card({ income, setIncome, removeIncome }: cardProp) { + const [seen, setSeen] = useState(false); + + const toggle = () => { + setSeen(!seen); + }; + + //Deletes the user income from the server and then remove it from the list + const deleteUserIncome = () => { + try { + //Send a request to delete user's income + deleteIncome(income).then((result) => { + //Sucessfully deleted income + if (result) { + removeIncome(income); } + }); + } catch { + alert("Failed to delete income " + income.name); } + }; - return( - <> + return ( + <> +
+

{income.name}

-

{income.name}

-
-

{income.income}

-

-

{income.description}

-
+

{income.income}

+

+

{income.description}

+
-
- - -
+
+ +
- - {seen ? ():null} - - ) +
-} \ No newline at end of file + {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/IncomeForm.tsx b/app/client/src/components/IncomeForm.tsx index 0d44da1..2e06f28 100644 --- a/app/client/src/components/IncomeForm.tsx +++ b/app/client/src/components/IncomeForm.tsx @@ -1,95 +1,128 @@ -import { useState } from "react" -import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput" -import './userForm.css' -import {postIncome, putIncome} from "../api/Income" +import { useState } from "react"; +import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput"; +import "./userForm.css"; +import { postIncome, putIncome } from "../api/Income"; import { validateIncomeForm } from "../utils/ValidateForms"; -import { type Income } from "../types/IncomeType"; +import { type Income } from "../types/IncomeType"; -interface popupProp{ - toggle: () => void; - setIncome?: (income:Income) => void - addIncome?: (income:Income) => void - edit: boolean - selectedIncome?: Income -} - -export default function PopupForm({toggle,setIncome, addIncome, edit, selectedIncome}:popupProp){ - - const handleSubmit = () => { - - //Convert to number - const inputAmount = Number(amount) +interface popupProp { + toggle: () => void; + setIncome?: (income: Income) => void; + addIncome?: (income: Income) => void; + edit: boolean; + selectedIncome?: Income; +} - if(validateIncomeForm(name, Number(amount))){ - - if(edit) { - putIncome(name,inputAmount,description).then((result) =>{ - if(result && selectedIncome && setIncome) { - const updateIncome: Income = {id:selectedIncome.id, name:name, income:inputAmount, description:description} - setIncome(updateIncome) - } - }) +export default function PopupForm({ + toggle, + setIncome, + addIncome, + edit, + selectedIncome, +}: popupProp) { + const handleSubmit = () => { + //Convert to number + const inputAmount = Number(amount); - } else { - postIncome(name,inputAmount,description).then((data)=>{ - - const newIncome: Income = {id: 0, name:name, income:inputAmount, description:description} + if (validateIncomeForm(name, Number(amount))) { + if (edit) { + putIncome(name, inputAmount, description).then((result) => { + if (result && selectedIncome && setIncome) { + const updateIncome: Income = { + id: selectedIncome.id, + name: name, + income: inputAmount, + description: description, + }; + setIncome(updateIncome); + } + }); + } else { + postIncome(name, inputAmount, description).then((data) => { + const newIncome: Income = { + id: 0, + name: name, + income: inputAmount, + description: description, + }; - //Determine if response gave back an id for the new created income - if(data && data.id) { - - newIncome.id = data.id + //Determine if response gave back an id for the new created income + if (data && data.id) { + newIncome.id = data.id; - //Check if add income is defined - if(addIncome){ - addIncome(newIncome) - } - - } - }) + //Check if add income is defined + if (addIncome) { + addIncome(newIncome); } - toggle() - } else { - alert("Please fillout the income form") - } + } + }); + } + toggle(); + } else { + alert("Please fillout the income form"); } + }; - const [name, setName] = useState('') - const [amount, setAmount] = useState('') - const [description, setDescription] = useState('') - - if(edit && selectedIncome) { - setName(selectedIncome.name) - setAmount(selectedIncome.toString()) - setDescription(selectedIncome.description) - } + const [name, setName] = useState(""); + const [amount, setAmount] = useState(""); + const [description, setDescription] = useState(""); - return( - <> -
+ if (edit && selectedIncome) { + setName(selectedIncome.name); + setAmount(selectedIncome.toString()); + setDescription(selectedIncome.description); + } -
- {edit ? (

Edit Income

):(

Create Income

)} -

+ return ( + <> +
+
+ {edit ?

Edit Income

:

Create Income

} +

- - setName(event.target.value)}/> -

+ + setName(event.target.value)} + /> +

- - handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event,amount,setAmount)}/> -

+ + handleCurrencyChange(event, setAmount)} + onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} + /> +

- - setDescription(event.target.value)}/> -

+ + setDescription(event.target.value)} + /> +

-
- - {edit? (): ()} -
-
+
+ + {edit ? ( + + ) : ( + + )} + +
- - ) +
+ + ); } diff --git a/app/client/src/components/IncomeList.tsx b/app/client/src/components/IncomeList.tsx index 69a555e..63d325d 100644 --- a/app/client/src/components/IncomeList.tsx +++ b/app/client/src/components/IncomeList.tsx @@ -1,54 +1,62 @@ -import { useState } from "react" -import { getIncome} from "../api/Income" -import { type Income } from "../types/IncomeType" -import IncomePopup from "./IncomeForm" -import IncomeCard from "./IncomeCard" +import { useState } from "react"; +import { getIncome } from "../api/Income"; +import { type Income } from "../types/IncomeType"; +import IncomePopup from "./IncomeForm"; +import IncomeCard from "./IncomeCard"; export default function IncomeList() { + const [userIncomes, setUsersIncomes] = useState([]); + const [seen, setSeen] = useState(false); - const [userIncomes, setUsersIncomes] = useState([]) - const [seen, setSeen] = useState(false) - - - //Try to get the account's transaction from the server - try { - getIncome().then((incomes)=>{ - //Determine if we acquired the accounts transaction - if(incomes) { - setUsersIncomes(incomes) - } - }) - } catch{ - alert("Failed to get user income") - } - - //Adds a account to the list - const addIncome = (newIncome:Income) => { - setUsersIncomes(userIncomes => [...userIncomes,newIncome]) - } - - //Removes any income from the list thats shares the same id - const removeIncome = (remove:Income) => { - setUsersIncomes(userIncomes=> userIncomes.filter((incomes) => incomes.id !== remove.id)) - } - - const toggle = () => { - setSeen(!seen) - } - - return( - <> -
-
- {userIncomes.map(income => ())} -
- -
- -
-
- - {seen ? ():null} - - ) -} \ No newline at end of file + //Try to get the account's transaction from the server + try { + getIncome().then((incomes) => { + //Determine if we acquired the accounts transaction + if (incomes) { + setUsersIncomes(incomes); + } + }); + } catch { + alert("Failed to get user income"); + } + + //Adds a account to the list + const addIncome = (newIncome: Income) => { + setUsersIncomes((userIncomes) => [...userIncomes, newIncome]); + }; + + //Removes any income from the list thats shares the same id + const removeIncome = (remove: Income) => { + setUsersIncomes((userIncomes) => + userIncomes.filter((incomes) => incomes.id !== remove.id), + ); + }; + + const toggle = () => { + setSeen(!seen); + }; + + return ( + <> +
+
+ {userIncomes.map((income) => ( + + ))} +
+ +
+ +
+
+ + {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/Layout.tsx b/app/client/src/components/Layout.tsx index aa7e35d..63ad47f 100644 --- a/app/client/src/components/Layout.tsx +++ b/app/client/src/components/Layout.tsx @@ -1,5 +1,5 @@ -import { Outlet } from 'react-router-dom' -import Header from './Header' +import { Outlet } from "react-router-dom"; +import Header from "./Header"; function Layout() { // const location = useLocation() @@ -8,7 +8,7 @@ function Layout() {
- ) + ); } -export default Layout +export default Layout; diff --git a/app/client/src/components/LoadingSpinner.tsx b/app/client/src/components/LoadingSpinner.tsx index 53f9a2b..c1734d1 100644 --- a/app/client/src/components/LoadingSpinner.tsx +++ b/app/client/src/components/LoadingSpinner.tsx @@ -3,5 +3,5 @@ export default function LoadingSpinner() {
- ) -} \ No newline at end of file + ); +} diff --git a/app/client/src/components/NavBar.tsx b/app/client/src/components/NavBar.tsx index 4f643f7..98173e9 100644 --- a/app/client/src/components/NavBar.tsx +++ b/app/client/src/components/NavBar.tsx @@ -1,5 +1,5 @@ function NavBar() { - /*
+ /* */ return ( -
- -
- ) +
+ ); } -export default NavBar +export default NavBar; diff --git a/app/client/src/components/SankeyChart.tsx b/app/client/src/components/SankeyChart.tsx index 24142c2..d70ff3b 100644 --- a/app/client/src/components/SankeyChart.tsx +++ b/app/client/src/components/SankeyChart.tsx @@ -1,10 +1,19 @@ -import { ResponsiveContainer, Sankey, Tooltip, useChartWidth, Layer, Rectangle, type SankeyNodeProps } from 'recharts'; +import { + ResponsiveContainer, + Sankey, + Tooltip, + useChartWidth, + Layer, + Rectangle, + type SankeyNodeProps, +} from "recharts"; const TEXT_OFFSET = 6; interface SankeyData { - nodes: Array<{ name: string }>;//names of nodes - links: Array<{//links between nodes - source and target are indices of the nodes in the nodes array, value is the weight of the link + nodes: Array<{ name: string }>; //names of nodes + links: Array<{ + //links between nodes - source and target are indices of the nodes in the nodes array, value is the weight of the link source: number; target: number; value: number; @@ -15,13 +24,13 @@ interface SankeyChartProps { data: SankeyData; } -export function SankeyChart({data}: SankeyChartProps) { +export function SankeyChart({ data }: SankeyChartProps) { return ( containerWidth-TEXT_OFFSET*3; + const isOut = x + width + TEXT_OFFSET > containerWidth - TEXT_OFFSET * 3; return ( - + void - removeTransaction: (removeTransaction:Transaction) => void +interface cardProp { + transaction: Transaction; + setTransaction: (editedTransaction: Transaction) => void; + removeTransaction: (removeTransaction: Transaction) => void; } -export default function Card({transaction, setTransaction, removeTransaction}:cardProp) { - - const [seen, setSeen] = useState(false) - - const toggle = () => { - setSeen(!seen) - } - - //Deletes the user account from the server and then remove it from the list - const deleteTrans = () => { - - try { - //Send a request to delete account's transaction - deleteTransaction(transaction).then((result)=>{ - - //Sucessfully deleted transaction - if(result) { - removeTransaction(transaction) - } - }) - } catch { - alert("Failed to delete transaction " + transaction.id) +export default function Card({ + transaction, + setTransaction, + removeTransaction, +}: cardProp) { + const [seen, setSeen] = useState(false); + + const toggle = () => { + setSeen(!seen); + }; + + //Deletes the user account from the server and then remove it from the list + const deleteTrans = () => { + try { + //Send a request to delete account's transaction + deleteTransaction(transaction).then((result) => { + //Sucessfully deleted transaction + if (result) { + removeTransaction(transaction); } + }); + } catch { + alert("Failed to delete transaction " + transaction.id); } + }; - return( - <> + return ( + <> +
+

{transaction.id}

-

{transaction.id}

-
-

{transaction.amount + " " + transaction.date}

-

-

{transaction.description}

-
- -
- - -
+

{transaction.amount + " " + transaction.date}

+

+

{transaction.description}

- - {seen ? ():null} - - ) -} \ No newline at end of file +
+ + +
+
+ + {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx index a98077e..a4dad4f 100644 --- a/app/client/src/components/TransactionList.tsx +++ b/app/client/src/components/TransactionList.tsx @@ -1,58 +1,75 @@ import { useState } from "react"; -import { getTransactions} from "../api/Transaction"; +import { getTransactions } from "../api/Transaction"; import { type Transaction } from "../types/TransactionType"; -import TransactionPopup from "./TransationForm" -import TransactionCard from "./TransactionCard" +import TransactionPopup from "./TransationForm"; +import TransactionCard from "./TransactionCard"; -interface listProp{ - accountId: number +interface listProp { + accountId: number; } -export default function TransactionList({accountId}:listProp) { - - const [accountTransactions, setAccountTransactions] = useState([]) - const [seen, setSeen] = useState(false) - - - //Try to get the account's transaction from the server - try { - getTransactions(accountId).then((transactions)=>{ - //Determine if we acquired the accounts transaction - if(transactions) { - setAccountTransactions(transactions) - } - }) - } catch{ - alert("Failed to get transaction") - } - - //Adds a account to the list - const addTransaction = (newTransaction:Transaction) => { - setAccountTransactions(userTransactions => [...userTransactions,newTransaction]) - } - - //Removes any transaction from the list thats shares the same id - const removeTransaction = (remove:Transaction) => { - setAccountTransactions(userTransactions=> userTransactions.filter((transaction) => transaction.id !== remove.id)) - } - - const toggle = () => { - setSeen(!seen) - } - - return( - <> -
-
- {accountTransactions.map(transaction => ())} -
- -
- -
-
- - {seen ? ():null} - - ) -} \ No newline at end of file +export default function TransactionList({ accountId }: listProp) { + const [accountTransactions, setAccountTransactions] = useState( + [], + ); + const [seen, setSeen] = useState(false); + + //Try to get the account's transaction from the server + try { + getTransactions(accountId).then((transactions) => { + //Determine if we acquired the accounts transaction + if (transactions) { + setAccountTransactions(transactions); + } + }); + } catch { + alert("Failed to get transaction"); + } + + //Adds a account to the list + const addTransaction = (newTransaction: Transaction) => { + setAccountTransactions((userTransactions) => [ + ...userTransactions, + newTransaction, + ]); + }; + + //Removes any transaction from the list thats shares the same id + const removeTransaction = (remove: Transaction) => { + setAccountTransactions((userTransactions) => + userTransactions.filter((transaction) => transaction.id !== remove.id), + ); + }; + + const toggle = () => { + setSeen(!seen); + }; + + return ( + <> +
+
+ {accountTransactions.map((transaction) => ( + + ))} +
+ +
+ +
+
+ + {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index e726df3..8e706a4 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -1,7 +1,6 @@ -import { getTransactions } from '@/api/ManagerAPI'; -import type { Transaction } from '@/types/Transaction'; -import { useEffect, useState } from 'react' - +import { getTransactions } from "@/api/ManagerAPI"; +import type { Transaction } from "@/types/Transaction"; +import { useEffect, useState } from "react"; function TransactionTable() { const [transactions, setTransactions] = useState([]); @@ -16,7 +15,7 @@ function TransactionTable() { } }; fetchTransactions(); - },[]) + }, []); return (
{/* Header */} @@ -46,9 +45,7 @@ function TransactionTable() { {tx.description || "N/A"}
-
- {tx.category} -
+
{tx.category}
-
- {tx.from} -
+
{tx.from}
-
- {tx.to} -
+
{tx.to}
))}
- ) + ); } -export default TransactionTable +export default TransactionTable; diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index 44cc6b8..20a52f2 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -1,89 +1,102 @@ -import { useState} from "react"; +import { useState } from "react"; import CsvUpload from "./csvread/CsvUpload"; -import './userForm.css' -import { getUserAccounts} from "../api/Account"; +import "./userForm.css"; +import { getUserAccounts } from "../api/Account"; import { validateTransactionForm } from "../utils/ValidateForms"; -import { postTranscations, putTranscations} from "../api/Transaction"; +import { postTranscations, putTranscations } from "../api/Transaction"; import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput"; import { type Transaction } from "../types/TransactionType"; import { type Account } from "../types/AccountType"; import { transactionCategory } from "@/enum/TransactionCategory"; -interface popupProp{ - toggle: () => void; - setTransaction?: (editedTransaction:Transaction) => void - addTransaction?: (newTransaction:Transaction) => void - edit: boolean - selectedTransaction?: Transaction +interface popupProp { + toggle: () => void; + setTransaction?: (editedTransaction: Transaction) => void; + addTransaction?: (newTransaction: Transaction) => void; + edit: boolean; + selectedTransaction?: Transaction; } //Gets the keys of the enum -type typeOfTransaction = keyof typeof transactionCategory +type typeOfTransaction = keyof typeof transactionCategory; //Returns a form of for the user to enter their info -export default function PopupForm({toggle,setTransaction,addTransaction, edit, selectedTransaction}:popupProp){ - - //Handles the submiting the form - const handleSubmit = () =>{ - - const accountId = Number(selectedAccount) - const transferAmount = Number(amount) - let userTransaction: Transaction - - if(selectedType && validateTransactionForm(accountId, selectedType, transferAmount, selectedDate, undefined)){ - - userTransaction = {id: 0, financialAccount_id:accountId, amount:transferAmount, type: selectedType, date: new Date(selectedDate)} - - if(edit && selectedTransaction) { - try{ - //Editing selected transaction - userTransaction.id = selectedTransaction.id - - //Send a request to update the transaction - putTranscations(userTransaction).then((result) => { - //Determine if the - if(result && setTransaction) { - setTransaction(userTransaction) - } - }) - } catch{ - alert("Failed to edit transaction") - } +export default function PopupForm({ + toggle, + setTransaction, + addTransaction, + edit, + selectedTransaction, +}: popupProp) { + //Handles the submiting the form + const handleSubmit = () => { + const accountId = Number(selectedAccount); + const transferAmount = Number(amount); + let userTransaction: Transaction; + + if ( + selectedType && + validateTransactionForm( + accountId, + selectedType, + transferAmount, + selectedDate, + undefined, + ) + ) { + userTransaction = { + id: 0, + financialAccount_id: accountId, + amount: transferAmount, + type: selectedType, + date: new Date(selectedDate), + }; + + if (edit && selectedTransaction) { + try { + //Editing selected transaction + userTransaction.id = selectedTransaction.id; + + //Send a request to update the transaction + putTranscations(userTransaction).then((result) => { + //Determine if the + if (result && setTransaction) { + setTransaction(userTransaction); + } + }); + } catch { + alert("Failed to edit transaction"); + } + } else { + try { + //Send a request to create the transaction + postTranscations([userTransaction]).then((response) => { + //Successful put if response is returned + if (response && response[0].id) { + userTransaction.id = response[0].id; + + if (addTransaction) { + addTransaction(userTransaction); + } } else { - - try{ - //Send a request to create the transaction - postTranscations([userTransaction]).then((response) => { - - //Successful put if response is returned - if(response && response[0].id) { - userTransaction.id = response[0].id - - if(addTransaction){ - addTransaction(userTransaction) - } - } else { - alert("Failed to create transaction") - } - - - }) - } catch { - alert("Failed to create transaction") - } + alert("Failed to create transaction"); } - - //Need to insert function that sets the state in parent component - - toggle() - } else { - alert("Please fill out the transaction form") + }); + } catch { + alert("Failed to create transaction"); } + } + + //Need to insert function that sets the state in parent component + toggle(); + } else { + alert("Please fill out the transaction form"); } + }; - //Handle when the user adds a file to the form - /*const handleFile = (event:React.ChangeEvent) =>{ + //Handle when the user adds a file to the form + /*const handleFile = (event:React.ChangeEvent) =>{ if(event.target && event.target.files && event.target.files[0]){ setFile(event.target.files[0]) @@ -94,82 +107,116 @@ export default function PopupForm({toggle,setTransaction,addTransaction, edit, s //Insert cvs parsing function/validation function }*/ - //State of the user's account - const [account, setAccount] = useState() + //State of the user's account + const [account, setAccount] = useState(); - getUserAccounts().then(accounts => { - console.log(accounts) + getUserAccounts().then((accounts) => { + console.log(accounts); - //Detemrine accounts exist - if(accounts) { - setAccount(accounts) - } - }) - - //Holds state of user input - const [selectedAccount, setSelectedAccount] = useState("") - const [selectedType, setSelectedType] = useState("") - const [amount, setAmount] = useState("") - //const [file, setFile] = useState(undefined) - const [selectedDate, setSelectedDate] = useState("") - - //Holds the types of transfers - const transCat: typeOfTransaction [] = Object.keys(transactionCategory) as typeOfTransaction[]; - - if(edit && selectedTransaction){ - setSelectedAccount(selectedTransaction.amount.toString()) - setSelectedType(selectedTransaction.type) - setAmount(selectedTransaction.amount.toString()) + //Detemrine accounts exist + if (accounts) { + setAccount(accounts); } - - - return( - <> -
-
- {edit ? (

Edit Transaction

):(

Create Transaction

)} - - - - -

- - - -

- - - handleCurrencyChange(event, setAmount)} onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} placeholder="0.00"/> -

- - - setSelectedDate(event.target.value)}/> -

- - {edit ? (null):( - <> - - {!edit && selectedAccount && - ( ) - } - - )} -

- -
- - {edit ?():()} -
-
+ }); + + //Holds state of user input + const [selectedAccount, setSelectedAccount] = useState(""); + const [selectedType, setSelectedType] = useState(""); + const [amount, setAmount] = useState(""); + //const [file, setFile] = useState(undefined) + const [selectedDate, setSelectedDate] = useState(""); + + //Holds the types of transfers + const transCat: typeOfTransaction[] = Object.keys( + transactionCategory, + ) as typeOfTransaction[]; + + if (edit && selectedTransaction) { + setSelectedAccount(selectedTransaction.amount.toString()); + setSelectedType(selectedTransaction.type); + setAmount(selectedTransaction.amount.toString()); + } + + return ( + <> +
+
+ {edit ?

Edit Transaction

:

Create Transaction

} + + + + +

+ + + +

+ + + handleCurrencyChange(event, setAmount)} + onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} + placeholder="0.00" + /> +

+ + + setSelectedDate(event.target.value)} + /> +

+ + {edit ? null : ( + <> + + {!edit && selectedAccount && ( + + )} + + )} +

+ +
+ + {edit ? ( + + ) : ( + + )} +
- - ) +
+ + ); } - - diff --git a/app/client/src/components/csvread/CsvConfirm.tsx b/app/client/src/components/csvread/CsvConfirm.tsx index 26dda34..fb9e52f 100644 --- a/app/client/src/components/csvread/CsvConfirm.tsx +++ b/app/client/src/components/csvread/CsvConfirm.tsx @@ -8,16 +8,17 @@ interface Props { onBack: () => void; } -export default function CsvConfirmation({ rows, onConfirm, onBack }: Props) { // calculates summary stats and displays them along with confirm/back buttons - const validRows = rows.filter(r => r.errors.length === 0); - const invalidRows = rows.filter(r => r.errors.length > 0); +export default function CsvConfirmation({ rows, onConfirm, onBack }: Props) { + // calculates summary stats and displays them along with confirm/back buttons + const validRows = rows.filter((r) => r.errors.length === 0); + const invalidRows = rows.filter((r) => r.errors.length > 0); const totalIncome = validRows - .filter(r => (r.amount ?? 0) > 0) + .filter((r) => (r.amount ?? 0) > 0) .reduce((sum, r) => sum + (r.amount ?? 0), 0); const totalExpenses = validRows - .filter(r => (r.amount ?? 0) < 0) + .filter((r) => (r.amount ?? 0) < 0) .reduce((sum, r) => sum + (r.amount ?? 0), 0); return ( @@ -36,9 +37,7 @@ export default function CsvConfirmation({ rows, onConfirm, onBack }: Props) { Back - +
); } diff --git a/app/client/src/components/csvread/CsvDrop.tsx b/app/client/src/components/csvread/CsvDrop.tsx index 92a68bf..e6a2dc4 100644 --- a/app/client/src/components/csvread/CsvDrop.tsx +++ b/app/client/src/components/csvread/CsvDrop.tsx @@ -15,7 +15,7 @@ export default function CsvDrop({ onFileSelect }: CsvDropProps) { const file = e.dataTransfer.files?.[0]; onFileSelect(file || null); }; -//accepts only csv files + //accepts only csv files return (
setIsDragging(true)} @@ -30,7 +30,7 @@ export default function CsvDrop({ onFileSelect }: CsvDropProps) { textAlign: "center", background: isDragging ? "#eef" : "#fafafa", cursor: "pointer", - marginBottom: "1rem" + marginBottom: "1rem", }} >

Drag & drop your CSV here, or click to select

diff --git a/app/client/src/components/csvread/CsvError.tsx b/app/client/src/components/csvread/CsvError.tsx index 2a15960..f7be7ee 100644 --- a/app/client/src/components/csvread/CsvError.tsx +++ b/app/client/src/components/csvread/CsvError.tsx @@ -13,11 +13,10 @@ export default function CsvError({ message }: CsvErrorProps) { background: "#ffe5e5", color: "#b30000", border: "1px solid #ffb3b3", - marginBottom: "1rem" + marginBottom: "1rem", }} > {message}
); } - diff --git a/app/client/src/components/csvread/CsvFile.tsx b/app/client/src/components/csvread/CsvFile.tsx index 916cfa9..447d040 100644 --- a/app/client/src/components/csvread/CsvFile.tsx +++ b/app/client/src/components/csvread/CsvFile.tsx @@ -1,4 +1,4 @@ -//This displays file name, size, and a remove +//This displays file name, size, and a remove interface CsvFileProps { file: File; @@ -13,7 +13,7 @@ export default function CsvFile({ file, onRemove }: CsvFileProps) { border: "1px solid #ddd", borderRadius: 6, background: "#f9f9f9", - marginBottom: "1rem" + marginBottom: "1rem", }} > {file.name} @@ -28,7 +28,7 @@ export default function CsvFile({ file, onRemove }: CsvFileProps) { border: "none", padding: "0.4rem 0.8rem", borderRadius: 4, - cursor: "pointer" + cursor: "pointer", }} > Remove diff --git a/app/client/src/components/csvread/CsvPreview.tsx b/app/client/src/components/csvread/CsvPreview.tsx index 3475107..fd26de9 100644 --- a/app/client/src/components/csvread/CsvPreview.tsx +++ b/app/client/src/components/csvread/CsvPreview.tsx @@ -8,11 +8,11 @@ interface Props { export default function CsvPreviewTable({ rows }: Props) { // count valid and invalid rows - const validCount = rows.filter(r => r.errors.length === 0).length; + const validCount = rows.filter((r) => r.errors.length === 0).length; const invalidCount = rows.length - validCount; - return ( -
+ return ( +
Total rows: {rows.length}
Valid rows: {validCount}
@@ -45,7 +45,7 @@ export default function CsvPreviewTable({ rows }: Props) { {row.date} @@ -53,9 +53,7 @@ export default function CsvPreviewTable({ rows }: Props) { {row.amount} {row.sender} {row.recipient} - - {isInvalid ? row.errors.join(", ") : "—"} - + {isInvalid ? row.errors.join(", ") : "—"} ); })} diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index 15d228a..7700dc3 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -11,17 +11,18 @@ import type { TransactionDraft } from "../../utils/ConvertTransaction"; import SuccessScreen from "./csvSuccess"; export default function CsvUpload({ accountId }: { accountId: number }) { - const [file, setFile] = useState(null); // selected CSV file - const [error, setError] = useState(null); // validation errors - const [parsedData, setParsedData] = useState(null); // state for parsed and validated data - const [isParsing, setIsParsing] = useState(false); // track if parsing is in progress + const [file, setFile] = useState(null); // selected CSV file + const [error, setError] = useState(null); // validation errors + const [parsedData, setParsedData] = useState(null); // state for parsed and validated data + const [isParsing, setIsParsing] = useState(false); // track if parsing is in progress const [showConfirmation, setShowConfirmation] = useState(false); - const [importResult, setImportResult] = useState(null); + const [importResult, setImportResult] = useState(null); - - - // File Validation - const handleFileSelect = (selected: File | null) => { + // File Validation + const handleFileSelect = (selected: File | null) => { if (!selected) return; // Validate type @@ -38,7 +39,8 @@ export default function CsvUpload({ accountId }: { accountId: number }) { return; } - if (selected.size > 5 * 1024 * 1024) { // limit file size to 5MB + if (selected.size > 5 * 1024 * 1024) { + // limit file size to 5MB setError("File is too large (max 5MB)."); setFile(null); return; @@ -57,14 +59,15 @@ export default function CsvUpload({ accountId }: { accountId: number }) { setShowConfirmation(false); }; - const handlePreview = async () => { // parses the file and updates state with results + const handlePreview = async () => { + // parses the file and updates state with results if (!file) return; setIsParsing(true); setError(null); try { - const rows = await parseCsvFile(file); // returns array of transaction drafts with validation errors if any + const rows = await parseCsvFile(file); // returns array of transaction drafts with validation errors if any if (rows.length === 0) { setError("CSV contains no readable rows."); @@ -72,107 +75,103 @@ export default function CsvUpload({ accountId }: { accountId: number }) { } else { setParsedData(rows); } - } catch (err: Error | unknown) { // handle parsing errors - setError("Failed to parse CSV: " + (err instanceof Error ? err.message : String(err))); + } catch (err: Error | unknown) { + // handle parsing errors + setError( + "Failed to parse CSV: " + + (err instanceof Error ? err.message : String(err)), + ); setParsedData(null); } setIsParsing(false); }; + //handle import sends valid transactions to backend for insertion into database + const handleImport = async () => { + if (!parsedData) return; - - -//handle import sends valid transactions to backend for insertion into database -const handleImport = async () => { - if (!parsedData) return; - - try { - const response = await fetch("/api/transactions/csvTransaction", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - financialAccount_id: accountId, - transactions: parsedData - }), - }); - - const result = await response.json(); + try { + const response = await fetch("/api/transactions/csvTransaction", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + financialAccount_id: accountId, + transactions: parsedData, + }), + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || "Import failed"); + return; + } - if (!response.ok) { - setError(result.error || "Import failed"); - return; + setImportResult({ + inserted: result.inserted, + skipped: result.skipped, + }); + } catch (error) { + console.error("Import failed:", error); + setError("Error during import"); } - - setImportResult({ - inserted: result.inserted, - skipped: result.skipped - }); - - } catch (error){ - console.error("Import failed:", error); - setError("Error during import"); - } -}; - - + }; return ( -
- - {importResult && ( - { - setImportResult(null); - setParsedData(null); - setFile(null); - setShowConfirmation(false); - }} - /> - )} - {!importResult && ( - <> - - - {error && } - - {file && } - - {!showConfirmation && ( - - )} - - {parsedData && parsedData.length > 0 && !showConfirmation && ( - <> - - +
+ {importResult && ( + { + setImportResult(null); + setParsedData(null); + setFile(null); + setShowConfirmation(false); + }} + /> + )} + {!importResult && ( + <> + + + {error && } + + {file && } + + {!showConfirmation && ( - - )} - - {parsedData && parsedData.length > 0 && showConfirmation && ( - setShowConfirmation(false)} - onConfirm={handleImport} - /> - )} - - )} -
-); - -} \ No newline at end of file + )} + + {parsedData && parsedData.length > 0 && !showConfirmation && ( + <> + + + + + )} + + {parsedData && parsedData.length > 0 && showConfirmation && ( + setShowConfirmation(false)} + onConfirm={handleImport} + /> + )} + + )} +
+ ); +} diff --git a/app/client/src/components/csvread/csvSuccess.tsx b/app/client/src/components/csvread/csvSuccess.tsx index f03ff15..227b2a6 100644 --- a/app/client/src/components/csvread/csvSuccess.tsx +++ b/app/client/src/components/csvread/csvSuccess.tsx @@ -2,14 +2,16 @@ export default function SuccessScreen({ inserted, skipped, - onDone + onDone, }: { inserted: number; skipped: number; onDone: () => void; }) { return ( -
+

Import Complete

{inserted} transactions imported successfully.

{skipped} rows were skipped due to validation errors.

diff --git a/app/client/src/components/userForm.css b/app/client/src/components/userForm.css index 7856a36..999dbe1 100644 --- a/app/client/src/components/userForm.css +++ b/app/client/src/components/userForm.css @@ -1,67 +1,62 @@ - .popupForm { - background-color: lightgrey; - padding: 15px; - place-content: start; - height: auto; + background-color: lightgrey; + padding: 15px; + place-content: start; + height: auto; } -input{ - margin-top: 2.5%; - margin-bottom: 2.5%; - margin-left: 1%; - +input { + margin-top: 2.5%; + margin-bottom: 2.5%; + margin-left: 1%; } -select{ - margin-top: 2.5%; - margin-bottom: 2.5%; - margin-left: 1%; +select { + margin-top: 2.5%; + margin-bottom: 2.5%; + margin-left: 1%; } -.popup{ - color:black; - background-color:floralwhite; - padding: 15px; - border-radius: 5px; - border-style: solid; - border-width: 2px; - border-color: black; - - - position:fixed; - top:0; - bottom: 0; - right: 35%; - left: 35%; - z-index: 100; - width: max-content; - height: max-content; +.popup { + color: black; + background-color: floralwhite; + padding: 15px; + border-radius: 5px; + border-style: solid; + border-width: 2px; + border-color: black; - font-size: 10pt; + position: fixed; + top: 0; + bottom: 0; + right: 35%; + left: 35%; + z-index: 100; + width: max-content; + height: max-content; + font-size: 10pt; - justify-content: center; - align-items: center; + justify-content: center; + align-items: center; } -button{ - padding: 5px; - border-width:1px; - border-color: black; - border-radius: 5px; +button { + padding: 5px; + border-width: 1px; + border-color: black; + border-radius: 5px; } -.bottomButtons{ - display: flex; +.bottomButtons { + display: flex; } -.bottomButtons :first-child{ - margin-right: auto; - background-color: lightcoral; +.bottomButtons :first-child { + margin-right: auto; + background-color: lightcoral; } -.bottomButtons button:last-child{ - margin-left: auto; - background-color: aquamarine; +.bottomButtons button:last-child { + margin-left: auto; + background-color: aquamarine; } - diff --git a/app/client/src/enum/AccountCategory.ts b/app/client/src/enum/AccountCategory.ts index 382b668..6b084df 100644 --- a/app/client/src/enum/AccountCategory.ts +++ b/app/client/src/enum/AccountCategory.ts @@ -1,6 +1,6 @@ export const accountCategory = { - SAVING:"Saving", - CHEQUING: "Chequing", - INVESTMENT: "Investment", - DEBT: "Debt" -} \ No newline at end of file + SAVING: "Saving", + CHEQUING: "Chequing", + INVESTMENT: "Investment", + DEBT: "Debt", +}; diff --git a/app/client/src/enum/TransactionCategory.ts b/app/client/src/enum/TransactionCategory.ts index 59c9ec7..0dfb57f 100644 --- a/app/client/src/enum/TransactionCategory.ts +++ b/app/client/src/enum/TransactionCategory.ts @@ -1,8 +1,8 @@ export const transactionCategory = { - FOOD: 'Food', - HOUSING: "Housing", - UTIL: "Utilzities", - TRANSPORTATION: "Transportation", - INCOME:"INCOME", - OTHER: "Other", -}; \ No newline at end of file + FOOD: "Food", + HOUSING: "Housing", + UTIL: "Utilzities", + TRANSPORTATION: "Transportation", + INCOME: "INCOME", + OTHER: "Other", +}; diff --git a/app/client/src/index.css b/app/client/src/index.css index 674bce0..39600df 100644 --- a/app/client/src/index.css +++ b/app/client/src/index.css @@ -1,13 +1,11 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap'); - +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap"); @custom-variant dark (&:is(.dark *)); - :root { - font-family: 'Space Grotesk', 'Segoe UI', Tahoma, sans-serif; + font-family: "Space Grotesk", "Segoe UI", Tahoma, sans-serif; line-height: 1.5; font-weight: 400; color: #e3ffec; diff --git a/app/client/src/main.tsx b/app/client/src/main.tsx index bef5202..eff7ccc 100644 --- a/app/client/src/main.tsx +++ b/app/client/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( , -) +); diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx index 8fb6afd..a714878 100644 --- a/app/client/src/pages/DashboardPage.tsx +++ b/app/client/src/pages/DashboardPage.tsx @@ -1,17 +1,38 @@ // import React, { useEffect, useState } from 'react' // import type { Transaction } from '@/types/Transaction' -import { Chart, PointElement, LineElement,ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; +import { + Chart, + PointElement, + LineElement, + ArcElement, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from "chart.js"; // import { Pie, Line, Bar } from 'react-chartjs-2'; -import AccountCard from '@/components/AccountCard'; -import TransactionTable from '@/components/TransactionTable'; +import AccountCard from "@/components/AccountCard"; +import TransactionTable from "@/components/TransactionTable"; // import LoadingSpinner from '@/components/LoadingSpinner'; // import SankeyChart from '@/components/SankeyChart'; // import { getExpensesChartData, getSavingsContribChartData, getIncomeFlowChartData, getTransactions } from '@/api/ManagerAPI'; // import type { SankeyData } from 'recharts/types/chart/Sankey'; // import ChartSection from '@/components/DashboardChartSection'; -import DashboardChartSection from '@/components/DashboardChartSection'; -import type { AuthSession } from './authTypes'; -Chart.register(PointElement, LineElement, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); +import DashboardChartSection from "@/components/DashboardChartSection"; +import type { AuthSession } from "./authTypes"; +Chart.register( + PointElement, + LineElement, + ArcElement, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +); type DashboardPageProps = { session: AuthSession; @@ -19,26 +40,49 @@ type DashboardPageProps = { }; function DashboardPage({ session, onLogout }: DashboardPageProps) { - return (
-

Hello {session.user.first_name ?? session.user.name ?? "there"}

-

Here you can view your recent transactions and manage your finances.

+ Log Out + +

+ Hello {session.user.first_name ?? session.user.name ?? "there"} +

+

+ Here you can view your recent transactions and manage your finances. +

- - - - - + + + + +

Recent Transactions

- +
- ) + ); } -export default DashboardPage +export default DashboardPage; diff --git a/app/client/src/types/AccountType.ts b/app/client/src/types/AccountType.ts index 85f3648..0fcbbed 100644 --- a/app/client/src/types/AccountType.ts +++ b/app/client/src/types/AccountType.ts @@ -1,10 +1,10 @@ //Coppied from Finance-Account type from server -export interface Account{ - id: number; - name: string; - type: string; - balance: number; - value: number; - subtype?: string; - last_updated: Date; -} \ No newline at end of file +export interface Account { + id: number; + name: string; + type: string; + balance: number; + value: number; + subtype?: string; + last_updated: Date; +} diff --git a/app/client/src/types/IncomeType.ts b/app/client/src/types/IncomeType.ts index de80e19..d96fa96 100644 --- a/app/client/src/types/IncomeType.ts +++ b/app/client/src/types/IncomeType.ts @@ -1,7 +1,6 @@ - export interface Income { - id:number, - name:string, - income: number, - description: string + id: number; + name: string; + income: number; + description: string; } diff --git a/app/client/src/types/Transaction.tsx b/app/client/src/types/Transaction.tsx index c4dc108..65bc735 100644 --- a/app/client/src/types/Transaction.tsx +++ b/app/client/src/types/Transaction.tsx @@ -1,9 +1,9 @@ export type Transaction = { - id: string; - amount: number; - category: string; - date: string; // ISO format date string - from: string; - to: string; - description?: string; -}; \ No newline at end of file + id: string; + amount: number; + category: string; + date: string; // ISO format date string + from: string; + to: string; + description?: string; +}; diff --git a/app/client/src/types/TransactionType.ts b/app/client/src/types/TransactionType.ts index a13e492..bc8f5da 100644 --- a/app/client/src/types/TransactionType.ts +++ b/app/client/src/types/TransactionType.ts @@ -1,11 +1,11 @@ //Coppied from server side transaction type -export interface Transaction{ - id: number; - financialAccount_id: number; - amount: number; - type: string - description?: string; - sender?: string; - recipient?: string; - date: Date; +export interface Transaction { + id: number; + financialAccount_id: number; + amount: number; + type: string; + description?: string; + sender?: string; + recipient?: string; + date: Date; } diff --git a/app/client/src/types/responseTypes.ts b/app/client/src/types/responseTypes.ts index b14dfc4..b3ef6e0 100644 --- a/app/client/src/types/responseTypes.ts +++ b/app/client/src/types/responseTypes.ts @@ -1,5 +1,5 @@ // export interface updateResponse { - lastUpdated?: Date - id?: number -} \ No newline at end of file + lastUpdated?: Date; + id?: number; +} diff --git a/app/client/src/utils/ConvertTransaction.ts b/app/client/src/utils/ConvertTransaction.ts index c7983b5..bcfd09c 100644 --- a/app/client/src/utils/ConvertTransaction.ts +++ b/app/client/src/utils/ConvertTransaction.ts @@ -1,6 +1,5 @@ //function to convert a normalized row into a transaction draft - import type { NormalizedRow } from "./NormalizeRow"; import { validateRow } from "./ValidateRow"; @@ -13,7 +12,8 @@ export interface TransactionDraft { errors: string[]; } -export function convertDraft(row: NormalizedRow): TransactionDraft { // convert normalized row to transaction draft and validate it +export function convertDraft(row: NormalizedRow): TransactionDraft { + // convert normalized row to transaction draft and validate it const errors = validateRow(row); return { @@ -21,4 +21,3 @@ export function convertDraft(row: NormalizedRow): TransactionDraft { / errors, }; } - diff --git a/app/client/src/utils/NormalizeRow.ts b/app/client/src/utils/NormalizeRow.ts index 5f2fa60..d329bfd 100644 --- a/app/client/src/utils/NormalizeRow.ts +++ b/app/client/src/utils/NormalizeRow.ts @@ -8,16 +8,18 @@ export interface NormalizedRow { recipient?: string | null; } -export function normalizeRow(raw: unknown): NormalizedRow { //row is treated as an object with unknown values +export function normalizeRow(raw: unknown): NormalizedRow { + //row is treated as an object with unknown values const record = raw as Record; - - const clean = (value: unknown): string | null => { // helper to trim and clean string-like values + + const clean = (value: unknown): string | null => { + // helper to trim and clean string-like values if (value === undefined || value === null) return null; return String(value).trim() || null; }; - const rawDate = clean(record.date); // normalize fields using helpers + const rawDate = clean(record.date); // normalize fields using helpers const date = normalizeDate(rawDate); - const rawAmount = clean(record.amount); + const rawAmount = clean(record.amount); const amount = normalizeAmount(rawAmount); return { @@ -32,10 +34,11 @@ export function normalizeRow(raw: unknown): NormalizedRow { //row is treated // helper to normalize date formats into YYYY-MM-DD function normalizeDate(input: string | null): string | null { if (!input) return null; - const data = input.replace(/[.\s]/g, "-").replace(/\//g, "-"); - if (/^\d{4}-\d{2}-\d{2}$/.test(data)) return data; // already in YYYY-MM-DD + const data = input.replace(/[.\s]/g, "-").replace(/\//g, "-"); + if (/^\d{4}-\d{2}-\d{2}$/.test(data)) return data; // already in YYYY-MM-DD - if (/^\d{2}-\d{2}-\d{4}$/.test(data)) { // if MM-DD-YYYY or DD-MM-YYYY, convert to YYYY-MM-DD + if (/^\d{2}-\d{2}-\d{4}$/.test(data)) { + // if MM-DD-YYYY or DD-MM-YYYY, convert to YYYY-MM-DD const [m, d, y] = data.split("-"); return `${y}-${m}-${d}`; } @@ -44,16 +47,17 @@ function normalizeDate(input: string | null): string | null { return `${y}-${m}-${d}`; } - return null; // invalid date + return null; // invalid date } -// helper to normalize amount fields +// helper to normalize amount fields function normalizeAmount(input: string | null): number | null { if (!input) return null; - let amount = input.replace(/,/g, "").trim(); //trim - if (/^\(.+\)$/.test(amount)) { //if amount is in parentheses, treat as negative + let amount = input.replace(/,/g, "").trim(); //trim + if (/^\(.+\)$/.test(amount)) { + //if amount is in parentheses, treat as negative amount = "-" + amount.slice(1, -1); } const num = Number(amount); - return isNaN(num) ? null : num; + return isNaN(num) ? null : num; } diff --git a/app/client/src/utils/ParseCsv.ts b/app/client/src/utils/ParseCsv.ts index 0efbd7e..cd3f627 100644 --- a/app/client/src/utils/ParseCsv.ts +++ b/app/client/src/utils/ParseCsv.ts @@ -1,33 +1,36 @@ -//this utility parses csv files using papaparse +//this utility parses csv files using papaparse // Parses CSV, normalizes rows, validates them, and returns TransactionDraft[] import Papa, { type ParseResult } from "papaparse"; import { normalizeRow } from "./NormalizeRow"; import { convertDraft, type TransactionDraft } from "./ConvertTransaction"; -export function parseCsvFile(file: File): Promise { //uses promises to handle async parsing - return new Promise((resolve, reject) => { +export function parseCsvFile(file: File): Promise { + //uses promises to handle async parsing + return new Promise((resolve, reject) => { Papa.parse(file, { header: true, skipEmptyLines: true, dynamicTyping: true, - complete: (result: ParseResult) => { // handle parsing errors + complete: (result: ParseResult) => { + // handle parsing errors const { data, errors } = result; if (errors.length > 0) { reject(errors[0].message); return; } - const drafts: TransactionDraft[] = data.map((raw: unknown) => { // normalize and convert each row to a transaction draft + const drafts: TransactionDraft[] = data.map((raw: unknown) => { + // normalize and convert each row to a transaction draft const normalized = normalizeRow(raw); return convertDraft(normalized); }); - resolve(drafts); // resolve with array of transaction drafts, each containing validation errors if any + resolve(drafts); // resolve with array of transaction drafts, each containing validation errors if any }, - error: (err: Error) => reject(err.message), // handle file read errors + error: (err: Error) => reject(err.message), // handle file read errors }); }); } diff --git a/app/client/src/utils/ValidateFile.ts b/app/client/src/utils/ValidateFile.ts index 358331d..aa5eb7d 100644 --- a/app/client/src/utils/ValidateFile.ts +++ b/app/client/src/utils/ValidateFile.ts @@ -2,16 +2,18 @@ import type { TransactionDraft } from "./ConvertTransaction"; -export interface FileValidationResult { // summary of validation results for the whole file +export interface FileValidationResult { + // summary of validation results for the whole file validCount: number; invalidCount: number; tooManyInvalid: boolean; } -export function validateFile(rows: TransactionDraft[]): FileValidationResult { //returns a summary of validation results for the whole file - const validCount = rows.filter(r => r.errors.length === 0).length; +export function validateFile(rows: TransactionDraft[]): FileValidationResult { + //returns a summary of validation results for the whole file + const validCount = rows.filter((r) => r.errors.length === 0).length; const invalidCount = rows.length - validCount; - const tooManyInvalid = rows.length > 0 && invalidCount / rows.length > 0.2; //if more than 20% of the rows are invalid we send a warning + const tooManyInvalid = rows.length > 0 && invalidCount / rows.length > 0.2; //if more than 20% of the rows are invalid we send a warning return { validCount, diff --git a/app/client/src/utils/ValidateForms.ts b/app/client/src/utils/ValidateForms.ts index 3d1e088..10cb245 100644 --- a/app/client/src/utils/ValidateForms.ts +++ b/app/client/src/utils/ValidateForms.ts @@ -1,46 +1,64 @@ import { accountCategory } from "@/enum/AccountCategory"; import { transactionCategory } from "@/enum/TransactionCategory"; -export function validateAccountForm(name:string, accountType:string, balance: number, csvFile?: File, subtype?: string, interest?: number) { - let result = false +export function validateAccountForm( + name: string, + accountType: string, + balance: number, + csvFile?: File, + subtype?: string, + interest?: number, +) { + let result = false; - if(name){ + if (name) { + //Checks if there a cvs file uploaded first + //Add cvs checker here= + if (csvFile) { + result = true; - //Checks if there a cvs file uploaded first - //Add cvs checker here= - if(csvFile){ - result = true - - //Checks if all the manditory fields are valid and optional ones too if they exist - } else if(Object.values(accountCategory).includes(accountType) && balance >= 0 && (subtype == undefined || subtype) && (interest == undefined || (interest >= 0 && interest < 100))) { - result = true - } - console.log(accountType) - console.log(accountCategory) + //Checks if all the manditory fields are valid and optional ones too if they exist + } else if ( + Object.values(accountCategory).includes(accountType) && + balance >= 0 && + (subtype == undefined || subtype) && + (interest == undefined || (interest >= 0 && interest < 100)) + ) { + result = true; } - console.log(result) - return result + console.log(accountType); + console.log(accountCategory); + } + console.log(result); + return result; } -export function validateTransactionForm(account_id:number, transactionType:string, amount:number,date:string, csvFile?:File) { +export function validateTransactionForm( + account_id: number, + transactionType: string, + amount: number, + date: string, + csvFile?: File, +) { + let result = false; - let result = false - - //Determine if account was selected - if(account_id) { - - //Add csv checker here to validate - if(csvFile) { - - result = true - } else if(Object.values(transactionCategory).includes(transactionType) && amount > 0 && date) { - result = true - } + //Determine if account was selected + if (account_id) { + //Add csv checker here to validate + if (csvFile) { + result = true; + } else if ( + Object.values(transactionCategory).includes(transactionType) && + amount > 0 && + date + ) { + result = true; } + } - return result -} + return result; +} -export function validateIncomeForm(name:string, income:number){ - return name && income > 0 +export function validateIncomeForm(name: string, income: number) { + return name && income > 0; } diff --git a/app/client/src/utils/constants.ts b/app/client/src/utils/constants.ts index ea4357f..45497b2 100644 --- a/app/client/src/utils/constants.ts +++ b/app/client/src/utils/constants.ts @@ -1 +1 @@ -export const BASE_URL = 'http://localhost:3000';//change this to the public link once microservices are hosted on AWS \ No newline at end of file +export const BASE_URL = "http://localhost:3000"; //change this to the public link once microservices are hosted on AWS diff --git a/app/client/src/utils/handleInput.ts b/app/client/src/utils/handleInput.ts index e87a2b7..c370db8 100644 --- a/app/client/src/utils/handleInput.ts +++ b/app/client/src/utils/handleInput.ts @@ -1,23 +1,28 @@ -import type React from "react" +import type React from "react"; //Handles state when currency is changed -export const handleCurrencyChange = (event:React.ChangeEvent, setCurrency: React.Dispatch>): void => { +export const handleCurrencyChange = ( + event: React.ChangeEvent, + setCurrency: React.Dispatch>, +): void => { + let input = event.target.value; + const pattern = /^\d*\.?\d{0,2}$/; - let input = event.target.value - const pattern = /^\d*\.?\d{0,2}$/ + console.log(input); + console.log(pattern.test(input)); + //Determine if the input follows the format/pattern + if (pattern.test(input) || input === "") { + input = input.replace(/^0+(?=\d)/, ""); + setCurrency(input); + } +}; - console.log(input) - console.log(pattern.test(input)) - //Determine if the input follows the format/pattern - if(pattern.test(input) || input === ""){ - input = input.replace(/^0+(?=\d)/,"") - setCurrency(input) - - } -} - -export const handleCurrencyBlur = (event:React.ChangeEvent,currency:string, setCurrency: React.Dispatch>): void => { - if(event.target.value !== "") { - setCurrency(parseFloat(currency).toFixed(2)) - } -} \ No newline at end of file +export const handleCurrencyBlur = ( + event: React.ChangeEvent, + currency: string, + setCurrency: React.Dispatch>, +): void => { + if (event.target.value !== "") { + setCurrency(parseFloat(currency).toFixed(2)); + } +}; diff --git a/app/client/tsconfig.app.json b/app/client/tsconfig.app.json index 468b1cf..a132313 100644 --- a/app/client/tsconfig.app.json +++ b/app/client/tsconfig.app.json @@ -25,9 +25,7 @@ "noUncheckedSideEffectImports": true, "baseUrl": ".", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, "include": ["src"] diff --git a/app/client/vite.config.ts b/app/client/vite.config.ts index 37ce3f8..8f3ddf6 100644 --- a/app/client/vite.config.ts +++ b/app/client/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' -import path from "path" -import tailwindcss from "@tailwindcss/vite" +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; +import tailwindcss from "@tailwindcss/vite"; // https://vite.dev/config/ export default defineConfig({ @@ -11,8 +11,8 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, - server: { + server: { cors: { origin: "http://localhost:3000/.*" }, port: 8080, - } -}) + }, +}); diff --git a/app/server/services/ts/README.md b/app/server/services/ts/README.md index dc1e64b..10a7885 100644 --- a/app/server/services/ts/README.md +++ b/app/server/services/ts/README.md @@ -1,13 +1,16 @@ -### Typescript services +### Typescript services + Each service inherits from this directory's: -* packages -* tsconfig -* eslint.config -Unless a package will be used in multiple modules install it only in the subdirectory (by running `bun install` in the module directory, not this one). +- packages +- tsconfig +- eslint.config + +Unless a package will be used in multiple modules install it only in the subdirectory (by running `bun install` in the module directory, not this one). The `common` folder is home for modules that are used by one or more services. For example `common/port.ts` is used by all services to determine which port to bind their webserver to. ### Tests + To run tests use `bun run vitest` to see the coverage run `bun run vitest --coverage` diff --git a/app/server/services/ts/api-gateway/package.json b/app/server/services/ts/api-gateway/package.json index c2c847e..b6ed315 100644 --- a/app/server/services/ts/api-gateway/package.json +++ b/app/server/services/ts/api-gateway/package.json @@ -7,7 +7,6 @@ "start": "bun dist/index.js" }, "dependencies": { - "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "cors": "^2.8.6", diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 20344da..2056ac0 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -3,27 +3,31 @@ import { onExit } from "@/hooks"; import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; -import cors from 'cors'; +import cors from "cors"; const app = express(); -const allowedOrigins = (process.env.CORS_ORIGINS ?? "http://localhost,http://localhost:8080") +const allowedOrigins = ( + process.env.CORS_ORIGINS ?? "http://localhost,http://localhost:8080" +) .split(",") .map((origin) => origin.trim()) .filter((origin) => origin.length > 0); -app.use(cors({ - origin: allowedOrigins, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] -})); +app.use( + cors({ + origin: allowedOrigins, + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + }), +); -app.use((req,res,next)=>{ - console.log("API-GATEWAY Incoming request: " + req.method + " " + req.url) - console.log(req.body) - next() -}) +app.use((req, res, next) => { + console.log("API-GATEWAY Incoming request: " + req.method + " " + req.url); + console.log(req.body); + next(); +}); app.use( createProxyMiddleware({ @@ -41,7 +45,7 @@ app.use( target: process.env.USER_SERVICE_ADDR, changeOrigin: true, pathRewrite: { "^/api/accounts": "/accounts" }, - }) + }), ); app.use( @@ -154,17 +158,17 @@ app.use( pathFilter: "/api/tranasctions", target: process.env.USER_SERVICE_ADDR, changeOrigin: true, - pathRewrite: { "^/api/transactions": "/transactions" } - }) + pathRewrite: { "^/api/transactions": "/transactions" }, + }), ); app.use( createProxyMiddleware({ - pathFilter:"/api/profiles", + pathFilter: "/api/profiles", target: process.env.USER_SERVICE_ADDR, changeOrigin: true, - pathRewrite: { "^/api/profiles": "/profiles" } - }) + pathRewrite: { "^/api/profiles": "/profiles" }, + }), ); const server = app.listen(PORT, () => { diff --git a/app/server/services/ts/auth/src/logic.ts b/app/server/services/ts/auth/src/logic.ts index 4c15ad8..e6017f0 100644 --- a/app/server/services/ts/auth/src/logic.ts +++ b/app/server/services/ts/auth/src/logic.ts @@ -1,9 +1,5 @@ import { type LoginBody, type SignupBody } from "./types"; -import { - createUser, - getUserByEmail, - accountWithEmailExists, -} from "./queries"; +import { createUser, getUserByEmail, accountWithEmailExists } from "./queries"; import { validateSignupBody } from "./validation"; import { passwordMatchesHash, generateJWT, hashPassword } from "./secrets.ts"; import { type User } from "@/types"; diff --git a/app/server/services/ts/auth/src/queries.ts b/app/server/services/ts/auth/src/queries.ts index eea094c..e9800f5 100644 --- a/app/server/services/ts/auth/src/queries.ts +++ b/app/server/services/ts/auth/src/queries.ts @@ -1,5 +1,5 @@ import type { UserWithPassword } from "./types"; -export async function getUserByEmail ( +export async function getUserByEmail( email: string, pool, ): Promise { diff --git a/app/server/services/ts/auth/test/logic.test.ts b/app/server/services/ts/auth/test/logic.test.ts index 5d4494a..5681b99 100644 --- a/app/server/services/ts/auth/test/logic.test.ts +++ b/app/server/services/ts/auth/test/logic.test.ts @@ -1,99 +1,104 @@ -vi.mock('../src/secrets.ts'); -vi.mock('../src/validation.ts'); -vi.mock('../src/queries.ts'); +vi.mock("../src/secrets.ts"); +vi.mock("../src/validation.ts"); +vi.mock("../src/queries.ts"); -import { vi, describe, beforeEach, afterEach, expect, it } from 'vitest'; -import { login, signup } from '../src/logic'; -import type { SignupBody } from '../src/types'; +import { vi, describe, beforeEach, afterEach, expect, it } from "vitest"; +import { login, signup } from "../src/logic"; +import type { SignupBody } from "../src/types"; //mocked functions -import { validateSignupBody } from '../src/validation'; -import { accountWithEmailExists, createUser, getUserByEmail } from '../src/queries'; -import { hashPassword, generateJWT, passwordMatchesHash } from '../src/secrets.ts'; +import { validateSignupBody } from "../src/validation"; +import { + accountWithEmailExists, + createUser, + getUserByEmail, +} from "../src/queries"; +import { + hashPassword, + generateJWT, + passwordMatchesHash, +} from "../src/secrets.ts"; const resJson = vi.fn(); let res; beforeEach(() => { - res = { - status: vi.fn(() => ({ json: resJson})) - } + res = { + status: vi.fn(() => ({ json: resJson })), + }; }); afterEach(() => { - vi.resetAllMocks(); + vi.resetAllMocks(); }); describe("Signup behaviour", () => { - const signupBody: SignupBody = vi.fn({ - email: 'dummy email', - password: 'dummy password', - }) - - it("Should return 201 on happy path", async () => { - await signup(signupBody, res, undefined); - expect(res.status).toHaveBeenCalledWith(201); - }); - - it("Should insert use into DB", async () => { - vi.mocked(hashPassword).mockImplementation(() => "pwhash"); - await signup(signupBody, res, undefined); - - expect(createUser).toHaveBeenCalledOnce(); - const params = vi.mocked(createUser).mock.calls[0]; - expect(params[0]?.pw_hash).toEqual('pwhash'); - }); - - it("Should 400 on invalid sign in body", async () => { - vi.mocked(validateSignupBody).mockImplementation( - () => { throw new Error() } - ); - await signup(signupBody, res, undefined); - expect(res.status).toHaveBeenCalledWith(400); - }); - - it("Should 409 on user already existing", async () => { - vi.mocked(accountWithEmailExists).mockImplementation( - () => true - ); - await signup(signupBody, res, undefined); - expect(res.status).toHaveBeenCalledWith(409); + const signupBody: SignupBody = vi.fn({ + email: "dummy email", + password: "dummy password", + }); + + it("Should return 201 on happy path", async () => { + await signup(signupBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(201); + }); + + it("Should insert use into DB", async () => { + vi.mocked(hashPassword).mockImplementation(() => "pwhash"); + await signup(signupBody, res, undefined); + + expect(createUser).toHaveBeenCalledOnce(); + const params = vi.mocked(createUser).mock.calls[0]; + expect(params[0]?.pw_hash).toEqual("pwhash"); + }); + + it("Should 400 on invalid sign in body", async () => { + vi.mocked(validateSignupBody).mockImplementation(() => { + throw new Error(); }); + await signup(signupBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("Should 409 on user already existing", async () => { + vi.mocked(accountWithEmailExists).mockImplementation(() => true); + await signup(signupBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(409); + }); }); describe("Login behaviour", () => { - const MOCK_JWT = 'JWT'; - const MOCK_USER = { pw_hash: "dummy hash"} - beforeEach(() => { - vi.mocked(generateJWT).mockImplementation(() => MOCK_JWT); - vi.mocked(passwordMatchesHash).mockImplementation(() => true); - vi.mocked(getUserByEmail).mockImplementation(() => MOCK_USER) - }); - const loginBody = { - email: 'email', - password: 'password' - }; - - it("Should return JWT on Happy path", async () => { - await login(loginBody, res, undefined); - expect(res.status).toHaveBeenCalledWith(200); - expect(resJson).toHaveBeenCalledOnce(); - const params = vi.mocked(resJson).mock.calls[0]; - expect(params[0]?.token).toEqual(MOCK_JWT); - }); - - - it("Should 401 on no account w/ matching email", async () => { - vi.mocked(getUserByEmail).mockImplementation(() => { throw new Error() }); - await login(loginBody, res, undefined); - expect(res.status).toHaveBeenCalledWith(401); - }); - - - it("Should 401 on wrong password", async () => { - vi.mocked(passwordMatchesHash).mockImplementation(() => false); - await login(loginBody, res, undefined); - expect(res.status).toHaveBeenCalledWith(401); + const MOCK_JWT = "JWT"; + const MOCK_USER = { pw_hash: "dummy hash" }; + beforeEach(() => { + vi.mocked(generateJWT).mockImplementation(() => MOCK_JWT); + vi.mocked(passwordMatchesHash).mockImplementation(() => true); + vi.mocked(getUserByEmail).mockImplementation(() => MOCK_USER); + }); + const loginBody = { + email: "email", + password: "password", + }; + + it("Should return JWT on Happy path", async () => { + await login(loginBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(200); + expect(resJson).toHaveBeenCalledOnce(); + const params = vi.mocked(resJson).mock.calls[0]; + expect(params[0]?.token).toEqual(MOCK_JWT); + }); + + it("Should 401 on no account w/ matching email", async () => { + vi.mocked(getUserByEmail).mockImplementation(() => { + throw new Error(); }); + await login(loginBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("Should 401 on wrong password", async () => { + vi.mocked(passwordMatchesHash).mockImplementation(() => false); + await login(loginBody, res, undefined); + expect(res.status).toHaveBeenCalledWith(401); + }); }); - diff --git a/app/server/services/ts/eslint.config.mts b/app/server/services/ts/eslint.config.mts index 2a7e9c4..a6d7875 100644 --- a/app/server/services/ts/eslint.config.mts +++ b/app/server/services/ts/eslint.config.mts @@ -12,7 +12,7 @@ export default defineConfig([ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], - languageOptions: { globals: { ...globals.node, Bun: "readonly" }}, + languageOptions: { globals: { ...globals.node, Bun: "readonly" } }, }, { files: ["**/*.json"], diff --git a/app/server/services/ts/tsconfig.json b/app/server/services/ts/tsconfig.json index 82c9ae3..db915de 100644 --- a/app/server/services/ts/tsconfig.json +++ b/app/server/services/ts/tsconfig.json @@ -9,14 +9,14 @@ // See also https://aka.ms/tsconfig/module "module": "nodenext", "target": "esnext", - "types": [ "bun" ], + "types": ["bun"], // For nodejs: // "lib": ["esnext"], // "types": ["node"], // and npm install -D @types/node "paths": { - "@/*": [ "./common/*" ], + "@/*": ["./common/*"] }, // Other Outputs @@ -43,6 +43,6 @@ "isolatedModules": true, "noUncheckedSideEffectImports": true, "moduleDetection": "force", - "skipLibCheck": true, + "skipLibCheck": true } } diff --git a/app/server/services/ts/user/package.json b/app/server/services/ts/user/package.json index a962392..a9778c7 100644 --- a/app/server/services/ts/user/package.json +++ b/app/server/services/ts/user/package.json @@ -24,7 +24,6 @@ "dev": "bun --watch src/index.ts", "build": "tsc", "start": "bun dist/index.js" - }, "dependencies": { "@mysql/xdevapi": "^8.0.35", diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index 7520075..38a448c 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -1,27 +1,26 @@ -import express from 'express'; -import { accountsRouter } from './routes/account'; -import { profilesRouter } from './routes/profile'; -import { transactionsRouter } from './routes/transaction'; +import express from "express"; +import { accountsRouter } from "./routes/account"; +import { profilesRouter } from "./routes/profile"; +import { transactionsRouter } from "./routes/transaction"; import { PORT } from "@/port"; import { onExit } from "@/hooks"; import { buildCorsConfig } from "@/expressUtils"; - const app = express(); app.use(buildCorsConfig()); onExit(async () => await server.close()); app.use(express.json()); -app.use((req,res,next)=>{ - console.log("USER Incoming request: " + req.method + " " + req.url) - console.log(req.body) - next() -}) +app.use((req, res, next) => { + console.log("USER Incoming request: " + req.method + " " + req.url); + console.log(req.body); + next(); +}); //test endpoint -app.get('/health', (req: express.Request, res: express.Response) => { - res.send('ok'); +app.get("/health", (req: express.Request, res: express.Response) => { + res.send("ok"); }); app.use("/accounts", accountsRouter); @@ -31,8 +30,7 @@ app.use("/profiles", profilesRouter); const server = app.listen(PORT, () => { console.log(`User Service running on port ${PORT}`); }); -process.on("SIGTERM", () => server.close()); - +process.on("SIGTERM", () => server.close()); //test endpoint app.get("/health", (req: express.Request, res: express.Response) => { diff --git a/app/server/services/ts/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts index 41b7f6a..7c5761d 100644 --- a/app/server/services/ts/user/src/routes/account.ts +++ b/app/server/services/ts/user/src/routes/account.ts @@ -4,32 +4,29 @@ import { Router } from "express"; import type { Request, Response } from "express"; import type { ResultSetHeader } from "mysql2"; -import { getConnectionPool } from "@/sqlUtil" +import { getConnectionPool } from "@/sqlUtil"; export const accountsRouter = Router(); -const db = getConnectionPool() +const db = getConnectionPool(); accountsRouter.post("/", async (req: Request, res: Response) => { try { - console.log(req.body) + console.log(req.body); const { name, type, balance, value, subtype } = req.body; const [result] = await db.query( `INSERT INTO financialAccount (name, type, balance, value, subtype) VALUES (?, ?, ?, ?, ?)`, - [name, type, balance, value, subtype ?? null] + [name, type, balance, value, subtype ?? null], ); res.json({ message: "Account successfully created", id: result.insertId, - lastUpdated: new Date() - + lastUpdated: new Date(), }); - } catch (err) { console.error("Account creation failed", err); res.status(500).json({ error: "Account creation failed" }); } }); - diff --git a/app/server/services/ts/user/src/routes/profile.ts b/app/server/services/ts/user/src/routes/profile.ts index f26e6c9..ada2e0d 100644 --- a/app/server/services/ts/user/src/routes/profile.ts +++ b/app/server/services/ts/user/src/routes/profile.ts @@ -3,10 +3,10 @@ import { Router } from "express"; import type { Request, Response } from "express"; import type { ResultSetHeader } from "mysql2"; -import { getConnectionPool } from "../../../common/sqlUtil.js" +import { getConnectionPool } from "../../../common/sqlUtil.js"; export const profilesRouter = Router(); -const db = getConnectionPool() +const db = getConnectionPool(); // Create a profile profilesRouter.post("/", async (req: Request, res: Response) => { @@ -16,16 +16,15 @@ profilesRouter.post("/", async (req: Request, res: Response) => { const [result] = await db.query( `INSERT INTO profile (name, description) VALUES (?, ?)`, - [name, description ?? null] + [name, description ?? null], ); res.json({ message: "Profile successfully created", - id: result.insertId + id: result.insertId, }); } catch (err) { console.error("Profile creation failed:", err); res.status(500).json({ error: "Profile creation failed" }); } }); - diff --git a/app/server/services/ts/user/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts index 11ce9b2..73b4f60 100644 --- a/app/server/services/ts/user/src/routes/transaction.ts +++ b/app/server/services/ts/user/src/routes/transaction.ts @@ -1,27 +1,41 @@ -// transaction routes for creating and updating transactions +// transaction routes for creating and updating transactions import { Router } from "express"; import type { Request, Response } from "express"; import type { ResultSetHeader } from "mysql2"; -import { getConnectionPool } from "@/sqlUtil" +import { getConnectionPool } from "@/sqlUtil"; export const transactionsRouter = Router(); -const db = getConnectionPool() +const db = getConnectionPool(); //creating a new transaction transactionsRouter.post("/", async (req: Request, res: Response) => { try { - const { financialAccount_id, amount, description, sender, recipient, date } = req.body; + const { + financialAccount_id, + amount, + description, + sender, + recipient, + date, + } = req.body; const [result] = await db.query( `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) VALUES (?, ?, ?, ?, ?, ?)`, - [financialAccount_id, amount, description ?? null, sender ?? null, recipient ?? null, date] + [ + financialAccount_id, + amount, + description ?? null, + sender ?? null, + recipient ?? null, + date, + ], ); res.json({ message: "Transaction successfully created", - id: result.insertId + id: result.insertId, }); } catch (err) { console.error("Transaction creation failed", err); @@ -29,80 +43,90 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { } }); - - // Update an existing transaction transactionsRouter.put("/:id", async (req: Request, res: Response) => { try { const id = req.params.id; - const {amount,description, sender, recipient, date} = req.body; + const { amount, description, sender, recipient, date } = req.body; await db.query( `UPDATE transaction SET amount=?, description=?, sender=?, recipient=?, date=? WHERE id=?`, - [amount, description ?? null, sender ?? null, recipient ?? null, date, id] + [ + amount, + description ?? null, + sender ?? null, + recipient ?? null, + date, + id, + ], ); res.json({ message: "Transaction successfully updated" }); } catch (err) { - console.error("Transaction update failed", err); } + console.error("Transaction update failed", err); + } }); -// CSV Transaction -transactionsRouter.post("/csvTransaction", async (req: Request, res: Response) => { - try { - const { financialAccount_id, transactions } = req.body; +// CSV Transaction +transactionsRouter.post( + "/csvTransaction", + async (req: Request, res: Response) => { + try { + const { financialAccount_id, transactions } = req.body; - if (!financialAccount_id || !Array.isArray(transactions)) { - return res.status(400).json({ error: "Invalid request payload" }); - } + if (!financialAccount_id || !Array.isArray(transactions)) { + return res.status(400).json({ error: "Invalid request payload" }); + } - const validRows = transactions.filter((t: any) => t.errors.length === 0); + const validRows = transactions.filter( + (t: unknown) => t.errors?.length === 0, + ); - if (validRows.length === 0) { - return res.status(400).json({ error: "No valid transactions to import" }); - } + if (validRows.length === 0) { + return res + .status(400) + .json({ error: "No valid transactions to import" }); + } - const connection = await db.getConnection(); + const connection = await db.getConnection(); - try { - await connection.beginTransaction(); + try { + await connection.beginTransaction(); - for (const row of validRows) { - await connection.query( - `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) + for (const row of validRows) { + await connection.query( + `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date) VALUES (?, ?, ?, ?, ?, ?)`, - [ - financialAccount_id, - row.amount, - row.description ?? null, - row.sender ?? null, - row.recipient ?? null, - row.date, - ] - ); + [ + financialAccount_id, + row.amount, + row.description ?? null, + row.sender ?? null, + row.recipient ?? null, + row.date, + ], + ); + } + + await connection.commit(); + + res.json({ + message: "CSV transactions imported successfully", + inserted: validRows.length, + skipped: transactions.length - validRows.length, + }); + } catch (err) { + await connection.rollback(); + console.error("CSV transaction import failed", err); + res.status(500).json({ error: "CSV transaction import failed" }); + } finally { + connection.release(); } - - await connection.commit(); - - res.json({ - message: "CSV transactions imported successfully", - inserted: validRows.length, - skipped: transactions.length - validRows.length, - }); - } catch (err) { - await connection.rollback(); console.error("CSV transaction import failed", err); res.status(500).json({ error: "CSV transaction import failed" }); - - } finally { - connection.release(); } - - } catch (err) { - console.error("CSV transaction import failed", err); - res.status(500).json({ error: "CSV transaction import failed" }); - } -}); + }, +); diff --git a/app/server/services/ts/vitest.config.ts b/app/server/services/ts/vitest.config.ts index 1a17d0c..b06755e 100644 --- a/app/server/services/ts/vitest.config.ts +++ b/app/server/services/ts/vitest.config.ts @@ -1,10 +1,10 @@ -import { defineConfig } from 'vitest/config' -import path from "path" +import { defineConfig } from "vitest/config"; +import path from "path"; export default defineConfig({ - resolve: { - alias: { - "@": path.resolve(__dirname, "./common") - } - } -}) + resolve: { + alias: { + "@": path.resolve(__dirname, "./common"), + }, + }, +}); From 01fc5713affb12c491213dc6df78bf88dbc969a0 Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 4 Mar 2026 16:21:45 -0600 Subject: [PATCH 127/204] sequence diagram for auth --- .../sequence-diagrams/auth/diagram.png | Bin 0 -> 41328 bytes .../sequence-diagrams/auth/diagram.txt | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 documentation/sequence-diagrams/auth/diagram.png create mode 100644 documentation/sequence-diagrams/auth/diagram.txt diff --git a/documentation/sequence-diagrams/auth/diagram.png b/documentation/sequence-diagrams/auth/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5dcedec16f3e421c8167d1aba579468f513bb3f1 GIT binary patch literal 41328 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 Yi&pzC4p!R7M-}O6&lO*swPfr60{HljZvX%Q literal 0 HcmV?d00001 diff --git a/documentation/sequence-diagrams/auth/diagram.txt b/documentation/sequence-diagrams/auth/diagram.txt new file mode 100644 index 0000000..b9817bb --- /dev/null +++ b/documentation/sequence-diagrams/auth/diagram.txt @@ -0,0 +1,17 @@ +%%use mermaid.live to generate visualization +sequenceDiagram + user->>server: POST /signup + server->>database: Verify account does not exist + database-->>server: Success + server->>database: Create account + database-->>server: + server-->>user: Success + + user->>server: POST /login + server->>server: hash password + server->>database: Verify email with password hash + database-->>server: + server->>database: Get account info + database-->>server: + server->>server: generate JWT + server-->>user: JWT From 9e8ed5d93289e3484426ea500c456c45e3bf0c8a Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 4 Mar 2026 18:50:10 -0600 Subject: [PATCH 128/204] get rid of repo layer caching from fontend docker compose --- app/client/docker-compose.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/client/docker-compose.yml b/app/client/docker-compose.yml index 92d340d..7ab4fd7 100644 --- a/app/client/docker-compose.yml +++ b/app/client/docker-compose.yml @@ -1,10 +1,6 @@ services: webserver: image: $DOCKER_HUB_USERNAME/finus-webserver:$RELEASE_VERSION - build: - cache_from: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-webserver:build - cache_to: - - type=registry,ref=$DOCKER_HUB_USERNAME/finus-webserver:build,mode=max + build: . ports: - "80:80" From 6c7c0d774be83191b0e100e104b6a9fa4628ac80 Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 4 Mar 2026 19:43:57 -0600 Subject: [PATCH 129/204] fix cors issue when running frontent via bun --- app/server/services/ts/common/expressUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/server/services/ts/common/expressUtils.ts b/app/server/services/ts/common/expressUtils.ts index 34957fe..dc1dd5c 100644 --- a/app/server/services/ts/common/expressUtils.ts +++ b/app/server/services/ts/common/expressUtils.ts @@ -1,13 +1,13 @@ import cors from "cors"; const defaultMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]; -const defaultOrigin = "http://localhost"; +const defaultOrigins = ["http://localhost:80", "http://localhost:8080"]; export function buildCorsConfig(opts?: { - origin?: string; + origins?: string | string[]; methods?: string[]; }) { return cors({ - origin: opts?.origin ?? defaultOrigin, + origin: opts?.origin ?? defaultOrigins, methods: opts?.methods ?? defaultMethods, credentials: true, allowedHeaders: ["Content-Type", "Authorization"], From 02c3f019a77267dfc4609a51891c50b8cec04f8d Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Thu, 5 Mar 2026 01:56:47 -0600 Subject: [PATCH 130/204] add jwt token to the requests and adjusted user-input files. --- app/client/src/api/Account.ts | 20 ++-- app/client/src/api/Income.ts | 18 +++- app/client/src/api/ManagerAPI.tsx | 10 ++ app/client/src/api/Transaction.ts | 25 +++-- app/client/src/components/AccountForm.tsx | 23 +++-- app/client/src/components/AccountList.tsx | 17 +++- app/client/src/components/AccountListCard.tsx | 18 +++- app/client/src/components/TransactionCard.tsx | 8 +- app/client/src/components/TransactionList.tsx | 63 +++++++++--- app/client/src/components/TransationForm.tsx | 95 ++++++++++++++----- app/client/src/types/AccountType.ts | 4 +- app/client/src/types/IncomeType.ts | 2 +- app/client/src/types/Transaction.tsx | 1 + app/client/src/types/responseTypes.ts | 4 +- app/client/src/utils/ValidateForms.ts | 2 +- .../ts/user/src/routes/transaction.ts | 11 ++- 16 files changed, 239 insertions(+), 82 deletions(-) diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index b6eae08..3c52689 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,15 +1,18 @@ import type { updateResponse } from "../types/responseTypes"; import type { Account } from "../types/AccountType"; +import type { AuthSession } from "@/pages/authTypes"; const requestUrl = "http://localhost:3000/api/accounts"; //Sends a request to get different accounts the user has -export async function getUserAccounts(): Promise { +export async function getUserAccounts( + session: AuthSession, +): Promise { try { //Sends a http request and waits for a response const response = await fetch(requestUrl, { method: "GET", - credentials: "include", + headers: { Authorization: `Bearer ${session.token}` }, }); //Determine if we were able to retrieve user's data @@ -30,6 +33,7 @@ export async function getUserAccounts(): Promise { //Sends a post request to create a user account export async function postUserAccount( + session: AuthSession, newAccount: Account, ): Promise { try { @@ -38,8 +42,8 @@ export async function postUserAccount( method: "POST", headers: { "content-type": "application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", body: JSON.stringify(newAccount), }); @@ -60,6 +64,7 @@ export async function postUserAccount( //Updates the users account export async function putUserAccount( + session: AuthSession, newAccount: Account, ): Promise { try { @@ -68,8 +73,8 @@ export async function putUserAccount( method: "PUT", headers: { "content-type": "application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", body: JSON.stringify(newAccount), }); @@ -88,15 +93,18 @@ export async function putUserAccount( } } -export async function deleteUserAccount(userAccount: Account) { +export async function deleteUserAccount( + session: AuthSession, + userAccount: Account, +) { const content = JSON.stringify({ id: userAccount.id }); //Create delete request to delete the account const response = await fetch(requestUrl, { method: "DELETE", headers: { "content-type": "/application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", body: content, }); diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts index 09a05fe..9db69a1 100644 --- a/app/client/src/api/Income.ts +++ b/app/client/src/api/Income.ts @@ -1,13 +1,14 @@ import type { updateResponse } from "../types/responseTypes"; import { type Income } from "../types/IncomeType"; +import type { AuthSession } from "@/pages/authTypes"; const requestUrl = "http://localhost:3000/api/income"; -export async function getIncome(): Promise { +export async function getIncome(session: AuthSession): Promise { try { const response = await fetch(requestUrl, { method: "GET", - credentials: "include", + headers: { Authorization: `Bearer ${session.token}` }, }); if (!response.ok) { @@ -24,6 +25,7 @@ export async function getIncome(): Promise { } export async function postIncome( + session: AuthSession, name: string, income: number, description: string, @@ -33,8 +35,9 @@ export async function postIncome( method: "POST", headers: { "content-type": "application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", + body: JSON.stringify({ name: name, income: income, @@ -57,6 +60,7 @@ export async function postIncome( } export async function putIncome( + session: AuthSession, name: string, income: number, description: string, @@ -66,6 +70,7 @@ export async function putIncome( method: "POST", headers: { "content-type": "applicaton/json", + Authorization: `Bearer ${session.token}`, }, credentials: "include", body: JSON.stringify({ @@ -89,7 +94,10 @@ export async function putIncome( } } -export async function deleteIncome(selectedIncome: Income) { +export async function deleteIncome( + session: AuthSession, + selectedIncome: Income, +) { const content = JSON.stringify({ id: selectedIncome.id }); try { //Create delete request to delete the income @@ -97,8 +105,8 @@ export async function deleteIncome(selectedIncome: Income) { method: "DELETE", headers: { "content-type": "/application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", body: content, }); diff --git a/app/client/src/api/ManagerAPI.tsx b/app/client/src/api/ManagerAPI.tsx index 49ab3a2..e91211e 100644 --- a/app/client/src/api/ManagerAPI.tsx +++ b/app/client/src/api/ManagerAPI.tsx @@ -21,6 +21,7 @@ async function getTransactions(): Promise { const transactions: Transaction[] = [ { id: "tx001", + financialAccount_id: "", amount: 1200, category: "Income", date: "2026-02-01", @@ -30,6 +31,7 @@ async function getTransactions(): Promise { }, { id: "tx002", + financialAccount_id: "", amount: -75.5, category: "Expenses", date: "2026-02-02", @@ -39,6 +41,7 @@ async function getTransactions(): Promise { }, { id: "tx003", + financialAccount_id: "", amount: -45, category: "Expenses", date: "2026-02-03", @@ -48,6 +51,7 @@ async function getTransactions(): Promise { }, { id: "tx004", + financialAccount_id: "", amount: -120, category: "Expenses", date: "2026-02-04", @@ -57,6 +61,7 @@ async function getTransactions(): Promise { }, { id: "tx005", + financialAccount_id: "", amount: -60, category: "Expenses", date: "2026-02-05", @@ -66,6 +71,7 @@ async function getTransactions(): Promise { }, { id: "tx006", + financialAccount_id: "", amount: -35.75, category: "Expenses", date: "2026-02-06", @@ -75,6 +81,7 @@ async function getTransactions(): Promise { }, { id: "tx007", + financialAccount_id: "", amount: -15.99, category: "Expenses", date: "2026-02-07", @@ -84,6 +91,7 @@ async function getTransactions(): Promise { }, { id: "tx008", + financialAccount_id: "", amount: -200, category: "Expenses", date: "2026-02-08", @@ -93,6 +101,7 @@ async function getTransactions(): Promise { }, { id: "tx009", + financialAccount_id: "", amount: 150, category: "Expenses", date: "2026-02-09", @@ -102,6 +111,7 @@ async function getTransactions(): Promise { }, { id: "tx010", + financialAccount_id: "", amount: -500, category: "Expenses", date: "2026-02-01", diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index e0b0418..0ed33ca 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,11 +1,13 @@ +import type { AuthSession } from "@/pages/authTypes"; import type { updateResponse } from "../types/responseTypes"; -import { type Transaction } from "../types/TransactionType"; +import { type Transaction } from "../types/Transaction"; const requestUrl = "http://localhost:3000/api/transacitons"; //Sends a GET request to get the list of user transactions for the account export async function getTransactions( - account_id: number, + session: AuthSession, + account_id: string, ): Promise { try { //Put as object to convert to json when sent in the body @@ -13,7 +15,7 @@ export async function getTransactions( const response = await fetch(requestUrl, { method: "GET", - credentials: "include", + headers: { Authorization: `Bearer ${session.token}` }, body: JSON.stringify(content), }); @@ -31,6 +33,7 @@ export async function getTransactions( //Can send multiple transactions in a push request export async function postTranscations( + session: AuthSession, trans: Transaction[], ): Promise { try { @@ -38,8 +41,8 @@ export async function postTranscations( method: "POST", headers: { "content-type": "application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", body: JSON.stringify(trans), }); @@ -57,14 +60,17 @@ export async function postTranscations( } } -export async function putTranscations(trans: Transaction): Promise { +export async function putTranscations( + session: AuthSession, + trans: Transaction, +): Promise { try { const response = await fetch(requestUrl, { method: "PUT", headers: { "content-type": "application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", body: JSON.stringify(trans), }); @@ -82,7 +88,10 @@ export async function putTranscations(trans: Transaction): Promise { } } -export async function deleteTransaction(selectedTransaction: Transaction) { +export async function deleteTransaction( + session: AuthSession, + selectedTransaction: Transaction, +) { try { const content = JSON.stringify({ id: selectedTransaction.id, @@ -93,8 +102,8 @@ export async function deleteTransaction(selectedTransaction: Transaction) { method: "DELETE", headers: { "content-type": "application/json", + Authorization: `Bearer ${session.token}`, }, - credentials: "include", body: content, }); diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index 222f9c1..d3d81e1 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -8,9 +8,11 @@ import { handleCurrencyBlur, } from "../utils/handleInput.ts"; import { accountCategory } from "@/enum/AccountCategory.ts"; +import type { AuthSession } from "@/pages/authTypes.ts"; interface popupProp { toggle: () => void; + session: AuthSession; setAccount?: (account: Account) => void; addAccount?: (account: Account) => void; edit: boolean; @@ -21,6 +23,7 @@ type typeofAccount = keyof typeof accountCategory; export default function PopupForm({ toggle, + session, setAccount, addAccount, edit, @@ -58,24 +61,24 @@ export default function PopupForm({ ) ) { const newAccount: Account = { - id: 0, + id: "", name: formInput.name, type: accountType, balance: accountBalance, subtype: "", value: 0, - last_updated: new Date(), + last_updated: "", }; console.log(newAccount); - //Determine if we edding an account info + //Determine if we editing an account info if (edit && selectedAccount) { newAccount.id = selectedAccount.id; try { - //Put reques to update the account - putUserAccount(newAccount).then((response) => { - //Determine if sucessfully post + //Put request to update the account + putUserAccount(session, newAccount).then((response) => { + //Determine if sucessfully updated the account if (response && response.lastUpdated && addAccount) { newAccount.last_updated = response.lastUpdated; addAccount(newAccount); @@ -86,10 +89,12 @@ export default function PopupForm({ } } else { try { - //Send a post request to create or edit the account - postUserAccount(newAccount).then((response) => { + //Send a post request to create + postUserAccount(session, newAccount).then((response) => { //When creating new account, id and lastupdated is returned for that account - if (response && response.id) newAccount.id = response.id; + if (response && response.id) { + newAccount.id = response.id; + } if (response.lastUpdated) { newAccount.last_updated = response.lastUpdated; diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx index 0bb34e8..ff7a969 100644 --- a/app/client/src/components/AccountList.tsx +++ b/app/client/src/components/AccountList.tsx @@ -3,8 +3,13 @@ import { getUserAccounts } from "../api/Account"; import { type Account } from "../types/AccountType"; import AccountListCard from "./AccountListCard"; import AccountPopup from "./AccountForm"; +import type { AuthSession } from "@/pages/authTypes"; -export default function AccountList() { +interface listProp { + session: AuthSession; +} + +export default function AccountList({ session }: listProp) { //Stores a lits of the user's account const [userAccounts, setUserAccounts] = useState([]); @@ -12,7 +17,7 @@ export default function AccountList() { //Try to get the accounts from the server try { - getUserAccounts().then((accounts) => { + getUserAccounts(session).then((accounts) => { //Determine if we acquired the accounts if (accounts) { setUserAccounts(accounts); @@ -44,6 +49,7 @@ export default function AccountList() {
{userAccounts.map((account) => ( {seen ? ( - + ) : null} ); diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx index e44afbe..1ae657e 100644 --- a/app/client/src/components/AccountListCard.tsx +++ b/app/client/src/components/AccountListCard.tsx @@ -2,14 +2,21 @@ import { deleteUserAccount } from "../api/Account.ts"; import { useState } from "react"; import AccountPopup from "./AccountForm.tsx"; import { type Account } from "../types/AccountType.ts"; +import type { AuthSession } from "@/pages/authTypes.ts"; interface cardProp { account: Account; + session: AuthSession; setAccount: (editAccount: Account) => void; removeAccount: (removeAccount: Account) => void; } -export default function Card({ account, setAccount, removeAccount }: cardProp) { +export default function Card({ + account, + session, + setAccount, + removeAccount, +}: cardProp) { //Stores the value that toggles thhe account popup form const [seen, setSeen] = useState(false); @@ -21,7 +28,7 @@ export default function Card({ account, setAccount, removeAccount }: cardProp) { const deleteAccount = () => { try { //Send a request to delete user account - deleteUserAccount(account).then((result) => { + deleteUserAccount(session, account).then((result) => { //Sucessfully deleted account if (result) { removeAccount(account); @@ -49,7 +56,12 @@ export default function Card({ account, setAccount, removeAccount }: cardProp) {
{seen ? ( - + ) : null} ); diff --git a/app/client/src/components/TransactionCard.tsx b/app/client/src/components/TransactionCard.tsx index e62d166..cc19ec5 100644 --- a/app/client/src/components/TransactionCard.tsx +++ b/app/client/src/components/TransactionCard.tsx @@ -1,16 +1,19 @@ import { useState } from "react"; import { deleteTransaction } from "../api/Transaction"; -import { type Transaction } from "../types/TransactionType"; +import { type Transaction } from "../types/Transaction"; import TransactionPopup from "./TransationForm"; +import type { AuthSession } from "@/pages/authTypes"; interface cardProp { transaction: Transaction; + session: AuthSession; setTransaction: (editedTransaction: Transaction) => void; removeTransaction: (removeTransaction: Transaction) => void; } export default function Card({ transaction, + session, setTransaction, removeTransaction, }: cardProp) { @@ -24,7 +27,7 @@ export default function Card({ const deleteTrans = () => { try { //Send a request to delete account's transaction - deleteTransaction(transaction).then((result) => { + deleteTransaction(session, transaction).then((result) => { //Sucessfully deleted transaction if (result) { removeTransaction(transaction); @@ -54,6 +57,7 @@ export default function Card({ {seen ? ( diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx index a4dad4f..d76da8e 100644 --- a/app/client/src/components/TransactionList.tsx +++ b/app/client/src/components/TransactionList.tsx @@ -1,29 +1,51 @@ import { useState } from "react"; import { getTransactions } from "../api/Transaction"; -import { type Transaction } from "../types/TransactionType"; +import { type Transaction } from "../types/Transaction"; import TransactionPopup from "./TransationForm"; import TransactionCard from "./TransactionCard"; +import type { AuthSession } from "@/pages/authTypes"; +import { type Account } from "@/types/AccountType"; +import { getUserAccounts } from "@/api/Account"; interface listProp { - accountId: number; + session: AuthSession; } -export default function TransactionList({ accountId }: listProp) { +export default function TransactionList({ session }: listProp) { + const [userAccounts, setUserAccounts] = useState([]); + const [selectedAccount, setSelectedAccount] = useState(""); const [accountTransactions, setAccountTransactions] = useState( [], ); const [seen, setSeen] = useState(false); - //Try to get the account's transaction from the server - try { - getTransactions(accountId).then((transactions) => { - //Determine if we acquired the accounts transaction - if (transactions) { - setAccountTransactions(transactions); + getUserAccounts(session) + .then((accounts) => { + console.log(accounts); + + //Detemrine accounts exist + if (accounts) { + setUserAccounts(accounts); + } else { + alert("Failed to get accounts"); } + }) + .catch(() => { + alert("Failed to get accounts"); }); - } catch { - alert("Failed to get transaction"); + + //Try to get the account's transaction from the server + if (selectedAccount) { + getTransactions(session, selectedAccount) + .then((transactions) => { + //Determine if we acquired the accounts transaction + if (transactions) { + setAccountTransactions(transactions); + } + }) + .catch(() => { + alert("Failed to get transaction"); + }); } //Adds a account to the list @@ -48,10 +70,28 @@ export default function TransactionList({ accountId }: listProp) { return ( <>
+
+ + +
+
{accountTransactions.map((transaction) => ( @@ -66,6 +106,7 @@ export default function TransactionList({ accountId }: listProp) { {seen ? ( diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index 20a52f2..4fded96 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -5,12 +5,14 @@ import { getUserAccounts } from "../api/Account"; import { validateTransactionForm } from "../utils/ValidateForms"; import { postTranscations, putTranscations } from "../api/Transaction"; import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput"; -import { type Transaction } from "../types/TransactionType"; -import { type Account } from "../types/AccountType"; +import { type Transaction } from "../types/Transaction"; +import { type Account } from "@/types/AccountType"; import { transactionCategory } from "@/enum/TransactionCategory"; +import type { AuthSession } from "@/pages/authTypes"; interface popupProp { toggle: () => void; + session: AuthSession; setTransaction?: (editedTransaction: Transaction) => void; addTransaction?: (newTransaction: Transaction) => void; edit: boolean; @@ -23,6 +25,7 @@ type typeOfTransaction = keyof typeof transactionCategory; //Returns a form of for the user to enter their info export default function PopupForm({ toggle, + session, setTransaction, addTransaction, edit, @@ -30,26 +33,37 @@ export default function PopupForm({ }: popupProp) { //Handles the submiting the form const handleSubmit = () => { - const accountId = Number(selectedAccount); const transferAmount = Number(amount); let userTransaction: Transaction; + let to = ""; + let from = ""; if ( selectedType && validateTransactionForm( - accountId, + selectedAccount[0], selectedType, transferAmount, selectedDate, undefined, ) ) { + if (selectedType === transactionCategory.INCOME) { + to = selectedAccount[1]; + from = other; + } else { + to = other; + from = selectedAccount[1]; + } + userTransaction = { - id: 0, - financialAccount_id: accountId, + id: "", + financialAccount_id: selectedAccount[0], + to: to, + from: from, amount: transferAmount, - type: selectedType, - date: new Date(selectedDate), + category: selectedType, + date: selectedDate, }; if (edit && selectedTransaction) { @@ -58,7 +72,7 @@ export default function PopupForm({ userTransaction.id = selectedTransaction.id; //Send a request to update the transaction - putTranscations(userTransaction).then((result) => { + putTranscations(session, userTransaction).then((result) => { //Determine if the if (result && setTransaction) { setTransaction(userTransaction); @@ -70,7 +84,7 @@ export default function PopupForm({ } else { try { //Send a request to create the transaction - postTranscations([userTransaction]).then((response) => { + postTranscations(session, [userTransaction]).then((response) => { //Successful put if response is returned if (response && response[0].id) { userTransaction.id = response[0].id; @@ -110,21 +124,29 @@ export default function PopupForm({ //State of the user's account const [account, setAccount] = useState(); - getUserAccounts().then((accounts) => { - console.log(accounts); - - //Detemrine accounts exist - if (accounts) { - setAccount(accounts); - } - }); + getUserAccounts(session) + .then((accounts) => { + console.log(accounts); + //Detemrine accounts exist + if (accounts) { + setAccount(accounts); + } else { + alert("Failed to retrieve user's accounts, cannot make a transaction"); + toggle(); + } + }) + .catch(() => { + alert("Failed to retrieve user's accounts, cannot make a transaction"); + toggle(); + }); //Holds state of user input - const [selectedAccount, setSelectedAccount] = useState(""); + const [selectedAccount, setSelectedAccount] = useState([]); const [selectedType, setSelectedType] = useState(""); const [amount, setAmount] = useState(""); //const [file, setFile] = useState(undefined) const [selectedDate, setSelectedDate] = useState(""); + const [other, setOther] = useState(""); //Holds the types of transfers const transCat: typeOfTransaction[] = Object.keys( @@ -132,9 +154,16 @@ export default function PopupForm({ ) as typeOfTransaction[]; if (edit && selectedTransaction) { - setSelectedAccount(selectedTransaction.amount.toString()); - setSelectedType(selectedTransaction.type); + setSelectedType(selectedTransaction.category); setAmount(selectedTransaction.amount.toString()); + + //Depending the transaction, the user can be the recipient or sender + if (selectedTransaction.category === transactionCategory.INCOME) { + setSelectedAccount([selectedTransaction.id, selectedTransaction.to]); + setOther(selectedTransaction.from); + } else { + setOther(selectedTransaction.to); + } } return ( @@ -143,19 +172,35 @@ export default function PopupForm({
{edit ?

Edit Transaction

:

Create Transaction

} - - + setSelectedAccount(event.target.value.split(",")) + } + > {account && account.map((account) => ( - ))} -

+ {selectedType == transactionCategory.INCOME ? ( + + ) : ( + + )} + setOther(event.target.value)} + > + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/app/client/src/components/ui/label.tsx b/app/client/src/components/ui/label.tsx new file mode 100644 index 0000000..c8d48d3 --- /dev/null +++ b/app/client/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; + +import { cn } from "@/utils/utils"; + +const Label = React.forwardRef>( + ({ className, ...props }, ref) => ( +
- +
diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index 4fded96..123c12f 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -131,13 +131,13 @@ export default function PopupForm({ if (accounts) { setAccount(accounts); } else { - alert("Failed to retrieve user's accounts, cannot make a transaction"); - toggle(); + //alert("Failed to retrieve user's accounts, cannot make a transaction"); + //toggle(); } }) .catch(() => { - alert("Failed to retrieve user's accounts, cannot make a transaction"); - toggle(); + //alert("Failed to retrieve user's accounts, cannot make a transaction"); + //toggle(); }); //Holds state of user input @@ -200,6 +200,7 @@ export default function PopupForm({ type="text" onChange={(event) => setOther(event.target.value)} > +

diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx index 93877ef..4ea9135 100644 --- a/app/client/src/components/AccountList.tsx +++ b/app/client/src/components/AccountList.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { getUserAccounts } from "../api/Account"; import { type Account } from "../types/AccountType"; import AccountListCard from "./AccountListCard"; @@ -16,7 +16,7 @@ export default function AccountList({ session }: listProp) { const [seen, setSeen] = useState(false); //Try to get the accounts from the server - setInterval(async () => { + useEffect(() => { try { getUserAccounts(session).then((accounts) => { //Determine if we acquired the accounts @@ -26,9 +26,9 @@ export default function AccountList({ session }: listProp) { } }); } catch { - //alert("Failed to retrieve user accounts"); + alert("Failed to retrieve user accounts"); } - }, 60000); + }, [session]); //Adds a account to the list const addAccount = (newAccount: Account) => { diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx index 1ae657e..f31cc8d 100644 --- a/app/client/src/components/AccountListCard.tsx +++ b/app/client/src/components/AccountListCard.tsx @@ -46,7 +46,9 @@ export default function Card({

-

{account.type + " " + account.subtype}

+

+ {account.type + " " + (account.subtype ? " " + account.type : "")} +

{account.balance}

@@ -61,6 +63,7 @@ export default function Card({ session={session} setAccount={setAccount} edit={true} + selectedAccount={account} /> ) : null} diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx index d209e78..f7bdbc2 100644 --- a/app/client/src/components/TransactionList.tsx +++ b/app/client/src/components/TransactionList.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { getTransactions } from "../api/Transaction"; import { type Transaction } from "../types/Transaction"; import TransactionPopup from "./TransationForm"; @@ -19,7 +19,7 @@ export default function TransactionList({ session }: listProp) { ); const [seen, setSeen] = useState(false); - setInterval(async () => { + useEffect(() => { getUserAccounts(session) .then((accounts) => { console.log(accounts); @@ -34,21 +34,23 @@ export default function TransactionList({ session }: listProp) { .catch(() => { //alert("Failed to get accounts"); }); - }, 60000); + }, [session]); //Try to get the account's transaction from the server - if (selectedAccount) { - getTransactions(session, selectedAccount) - .then((transactions) => { - //Determine if we acquired the accounts transaction - if (transactions) { - setAccountTransactions(transactions); - } - }) - .catch(() => { - //alert("Failed to get transaction"); - }); - } + useEffect(() => { + if (selectedAccount) { + getTransactions(session, selectedAccount) + .then((transactions) => { + //Determine if we acquired the accounts transaction + if (transactions) { + setAccountTransactions(transactions); + } + }) + .catch(() => { + alert("Failed to get transaction"); + }); + } + }); //Adds a account to the list const addTransaction = (newTransaction: Transaction) => { diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index e8d26a5..2e1ed7c 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import CsvUpload from "./csvread/CsvUpload"; import "./userForm.css"; import { getUserAccounts } from "../api/Account"; @@ -63,7 +63,7 @@ export default function PopupForm({ from: from, amount: transferAmount, category: selectedType, - date: selectedDate, + date: new Date(selectedDate), }; if (edit && selectedTransaction) { @@ -124,21 +124,25 @@ export default function PopupForm({ //State of the user's account const [account, setAccount] = useState(); - getUserAccounts(session) - .then((accounts) => { - console.log(accounts); - //Detemrine accounts exist - if (accounts) { - setAccount(accounts); - } else { - //alert("Failed to retrieve user's accounts, cannot make a transaction"); + useEffect(() => { + getUserAccounts(session) + .then((accounts) => { + console.log(accounts); + //Detemrine accounts exist + if (accounts) { + setAccount(accounts); + } else { + alert( + "Failed to retrieve user's accounts, cannot make a transaction", + ); + //toggle(); + } + }) + .catch(() => { + alert("Failed to retrieve user's accounts, cannot make a transaction"); //toggle(); - } - }) - .catch(() => { - //alert("Failed to retrieve user's accounts, cannot make a transaction"); - //toggle(); - }); + }); + }, [session]); //Holds state of user input const [selectedAccount, setSelectedAccount] = useState([]); diff --git a/app/client/src/types/AccountType.ts b/app/client/src/types/AccountType.ts index 59e7a36..0fcbbed 100644 --- a/app/client/src/types/AccountType.ts +++ b/app/client/src/types/AccountType.ts @@ -6,5 +6,5 @@ export interface Account { balance: number; value: number; subtype?: string; - last_updated: string; + last_updated: Date; } diff --git a/app/client/src/types/Transaction.tsx b/app/client/src/types/Transaction.tsx index 5a7f295..b54eb5e 100644 --- a/app/client/src/types/Transaction.tsx +++ b/app/client/src/types/Transaction.tsx @@ -3,7 +3,7 @@ export type Transaction = { financialAccount_id: number; amount: number; category: string; - date: string; // ISO format date string + date: Date; // ISO format date string from: string; to: string; description?: string; diff --git a/app/client/src/types/responseTypes.ts b/app/client/src/types/responseTypes.ts index a37993b..b3ef6e0 100644 --- a/app/client/src/types/responseTypes.ts +++ b/app/client/src/types/responseTypes.ts @@ -1,5 +1,5 @@ // export interface updateResponse { - lastUpdated?: string; + lastUpdated?: Date; id?: number; } diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index d01263b..b25625c 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -41,6 +41,7 @@ services: environment: - MYSQL_USER - MYSQL_PASSWORD + - JWT_SECRET depends_on: - database diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql index 0741103..f35ba0b 100644 --- a/app/server/services/database/schema.sql +++ b/app/server/services/database/schema.sql @@ -74,10 +74,10 @@ CREATE TABLE finus.financialAccountSubtype( ); CREATE TABLE finus.financialAccount ( - id INTEGER NOT NULL, + id INTEGER NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL, - balance INTEGER NOT NULL, + balance DECIMAL(12,2) NOT NULL, value INTEGER NOT NULL, last_updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, subtype VARCHAR(50), diff --git a/app/server/services/ts/auth/src/queries.ts b/app/server/services/ts/auth/src/queries.ts index e9800f5..364bd77 100644 --- a/app/server/services/ts/auth/src/queries.ts +++ b/app/server/services/ts/auth/src/queries.ts @@ -65,6 +65,17 @@ export async function createUser(user: UserWithPassword, pool): void { [accountInsert.insertId, user.pw_hash], ); + const [result] = await connection.query( + "INSERT INTO profile (name) VALUE (?)", + [user.first_name], + ); + + await connection.query( + `INSERT INTO finusAccount_profile (account_id, profile_id) + VALUE(?,?)`, + [accountInsert.insertId, result.insertId], + ); + console.log("User created with ID:", accountInsert.insertId); await connection.commit(); diff --git a/app/server/services/ts/common/types.ts b/app/server/services/ts/common/types.ts index 376744e..7989578 100644 --- a/app/server/services/ts/common/types.ts +++ b/app/server/services/ts/common/types.ts @@ -1,3 +1,5 @@ +import type { RowDataPacket } from "mysql2"; + export type User = { id: number; username: string; @@ -10,7 +12,7 @@ export type User = { salt: string; }; -export type financialAccount = { +export interface financialAccount extends RowDataPacket { id: number; name: string; type: string; @@ -18,9 +20,9 @@ export type financialAccount = { value: number; last_updated: Date; subtype: string; -}; +} -export type Transaction = { +export interface Transaction extends RowDataPacket { id: number; financialAccount_id: number; amount: number; @@ -29,7 +31,7 @@ export type Transaction = { recipient: string; date: Date; // ISO format date string description?: string; -}; +} export function validateType(obj: unknown, requiredKeys: string[]): void { if (!obj) { diff --git a/app/server/services/ts/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts index 91224d4..17932c1 100644 --- a/app/server/services/ts/user/src/routes/account.ts +++ b/app/server/services/ts/user/src/routes/account.ts @@ -6,20 +6,30 @@ import type { Request, Response } from "express"; import type { ResultSetHeader } from "mysql2"; import { getConnectionPool } from "@/sqlUtil"; import type { financialAccount } from "@/types.js"; -import { authenticateJWT } from "../handleJWT.js"; +import jwt from "jsonwebtoken"; +//import { authenticateJWT } from "../handleJWT.js"; //import { error } from "node:console"; - +const JWT_SECRET = process.env.JWT_SECRET; export const accountsRouter = Router(); const db = getConnectionPool(); accountsRouter.post("/", async (req: Request, res: Response) => { try { - //console.log(req.body); const { name, type, balance, value, subtype } = req.body; + const last_updated = new Date(); + + let userId; - const last_updated = new Date().toISOString(); + try { + userId = authenticateJWT(req); + } catch (error) { + console.log(error); + return res + .status(401) + .json({ error: "Not authorized to create an account" }); + } const [result] = await db.query( `INSERT INTO financialAccount (name, type, balance, value, last_updated, subtype) @@ -27,53 +37,82 @@ accountsRouter.post("/", async (req: Request, res: Response) => { [name, type, balance, value, last_updated, subtype ?? null], ); - res.json({ + await db.query( + `INSERT INTO profile_financialAccount (profile_id, financialAccount_id) + VALUES (?,?)`, + [userId, result.insertId], + ); + + console.log("Created account " + name); + return res.status(200).json({ message: "Account successfully created", id: result.insertId, lastUpdated: last_updated, }); } catch (err) { console.error("Account creation failed", err); - res.status(500).json({ error: "Account creation failed" }); + return res.status(500).json({ error: "Account creation failed" }); } }); accountsRouter.get("/", async (req: Request, res: Response) => { - let userId = 0; + let userId; try { userId = authenticateJWT(req); } catch (err) { console.log(err); - res.status(401).json({ error: err }); + return res.status(401).json({ error: err }); } if (userId) { try { - const [result] = await db.query( + const [rows] = await db.query( `SELECT * FROM financialAccount WHERE id = ?`, [userId], ); - const accounts: financialAccount[] = new Array(result.length); - - for (let i = 0; i < result.length; i++) { - accounts[i] = result[i]; - } - - res.json(accounts); + return res.status(200).json(rows); } catch (err) { console.error("Failed to retrieve user's account", err); - res.status(500).json({ error: "Failed to retrieve account(s)" }); + return res.status(500).json({ error: "Failed to retrieve account(s)" }); } } }); accountsRouter.put("/", async (req: Request, res: Response) => { + let userId; + + //Checks if was given by the requests + if (isAccount(req.body)) { + console.error("No account was given"); + return res.status(400).json({ error: "No account was given" }); + } + + const account: financialAccount = req.body; + + //Determine if the user is authorized to make this request + try { + userId = authenticateJWT(req); + } catch (error) { + console.error(error); + return res + .status(401) + .json({ error: "User not authorized to update account" }); + } + + //Check if the user is the owner of the account that's bineg updated + if (!(await checkUserId(userId, account.id))) { + console.error("User is not own of the account"); + return res + .status(401) + .json({ error: "User is not authorized to update accounts" }); + } + try { const { id, name, type, balance, value, subtype } = req.body; - const last_updated = new Date().toISOString(); + const last_updated = new Date(); await db.query( `UPDATE financialAccount @@ -82,29 +121,128 @@ accountsRouter.put("/", async (req: Request, res: Response) => { [name, type, balance, value, last_updated, subtype ?? null, id], ); - res.json({ + console.log("Updated Account " + name); + return res.status(200).json({ message: "Account successfully updated", lastUpdated: last_updated, }); } catch (err) { console.error("Failed to update user's account", err); - res.status(500).json({ error: "Failed to update user's account" }); + return res.status(500).json({ error: "Failed to update user's account" }); } }); accountsRouter.delete("/", async (req: Request, res: Response) => { + let userId; + + const { id } = req.body; + try { - const { id } = req.body; + userId = authenticateJWT(req); + } catch (error) { + console.error(error); + return res + .status(401) + .json({ error: "User is not authorized to delete an account" }); + } + + //Checks if we were provided with a account id to delete + if (!id) { + console.error("Delete failed because no account id was provided"); + return res + .status(400) + .json({ error: "Bad request: No account was givens" }); + } + //Checks if user is owner of the acount + if (!(await checkUserId(userId, id))) { + console.error("User cannot delete account they didn't create"); + return res + .status(401) + .json({ error: "User not authorized to delete this account" }); + } + + try { await db.query( `DELETE FROM financialAccount WHERE id=?`, [id], ); - res.json({ message: "Account successfully deleted" }); + return res.status(200).json({ message: "Account successfully deleted" }); } catch (err) { console.error("Failed to delete user's account", err); - res.status(500).json({ error: "Failed to delete user's account" }); + return res.status(500).json({ error: "Failed to delete user's account" }); } }); + +export const authenticateJWT = (req: Request) => { + const authHeader = req.headers.authorization; + //console.log(req.headers); + if (!authHeader) { + throw new Error("Authorization header missing"); + } + + const parts = authHeader.split(" "); + if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { + throw new Error("Invalid authorization header format"); + } + const token = parts[1]; + + //Add as any if it doesn't work + const decoded = jwt.verify(token, JWT_SECRET); + const userId = decoded.sub; + //console.log("user id: " + userId); + if (!userId) { + throw new Error("User ID not found in token"); + } + + return userId; +}; + +//Checks the user is the owner of the account +async function checkUserId( + userId: number, + accountId: number, +): Promise { + let result = false; + try { + //Checks if the profile has an account with that id + const [rows] = await db.query( + `SELECT 1 FROM profile_financialAccount + WHERE profile_id =? AND financialAccount_id =?`, + [userId, accountId], + ); + + console.log(rows.length); + if (rows.length > 0) { + result = true; + } + } catch (error) { + console.error(error); + } + + return result; +} + +function isAccount(reqBody: unknown): reqBody is financialAccount { + //Checks if it exists and is an object + if (typeof reqBody !== "object" || reqBody === null) { + //Neither + return false; + } + + //Constrained to not use any, so using Record instead + const check = reqBody as Record; + + //Checks if the fields match to an financial account + return ( + typeof check.id === "number" && + typeof check.balence === "number" && + typeof check.type === "string" && + typeof check.name === "string" && + typeof check.value === "number" && + typeof check.subtype === "string" && + check.last_updated instanceof Date + ); +} From e4a69e05b2011df892f94f5d18beb497931527ac Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 07:30:10 -0600 Subject: [PATCH 160/204] implementation of user logic tests - queries avoided --- app/server/docker-compose.test.yml | 5 + app/server/services/ts/user/Dockerfile.test | 10 + app/server/services/ts/user/bun.lock | 610 +++++++++++++++++- app/server/services/ts/user/package.json | 3 +- .../services/ts/user/src/logic/expenses.ts | 1 + .../ts/user/src/logic/transactions.ts | 12 - .../services/ts/user/src/queries/snapshot.ts | 106 +-- .../services/ts/user/src/types/Transaction.ts | 2 +- .../services/ts/user/src/utils/dates.ts | 4 + .../services/ts/user/tests/dates.test.ts | 221 +++++++ .../ts/user/tests/factories/date.factory.ts | 158 +++++ .../user/tests/factories/expense.factory.ts | 65 ++ .../tests/factories/transaction.factory.ts | 49 ++ .../ts/user/tests/logic_expenses.test.ts | 338 ++++++++++ .../ts/user/tests/logic_snapshot.test.ts | 441 +++++++++++++ .../ts/user/tests/logic_transactions.test.ts | 87 +++ 16 files changed, 2042 insertions(+), 70 deletions(-) create mode 100644 app/server/services/ts/user/Dockerfile.test create mode 100644 app/server/services/ts/user/tests/dates.test.ts create mode 100644 app/server/services/ts/user/tests/factories/date.factory.ts create mode 100644 app/server/services/ts/user/tests/factories/expense.factory.ts create mode 100644 app/server/services/ts/user/tests/factories/transaction.factory.ts create mode 100644 app/server/services/ts/user/tests/logic_expenses.test.ts create mode 100644 app/server/services/ts/user/tests/logic_snapshot.test.ts create mode 100644 app/server/services/ts/user/tests/logic_transactions.test.ts diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 45368ce..dccf20a 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -8,3 +8,8 @@ services: build: context: services/ts dockerfile: auth/Dockerfile.test + + user-test: + build: + context: services/ts + dockerfile: user/Dockerfile.test diff --git a/app/server/services/ts/user/Dockerfile.test b/app/server/services/ts/user/Dockerfile.test new file mode 100644 index 0000000..da30765 --- /dev/null +++ b/app/server/services/ts/user/Dockerfile.test @@ -0,0 +1,10 @@ +FROM measureonecodetwice/finus-user:local + +COPY user/test ./test + +WORKDIR /project +COPY vitest.config.ts . + +ENV CI=true + +CMD [ "bun", "run", "vitest", "--run"] diff --git a/app/server/services/ts/user/bun.lock b/app/server/services/ts/user/bun.lock index 6c201c1..d88bf07 100644 --- a/app/server/services/ts/user/bun.lock +++ b/app/server/services/ts/user/bun.lock @@ -5,29 +5,185 @@ "": { "name": "user-service", "dependencies": { + "@mysql/xdevapi": "^8.0.35", "axios": "^1.13.6", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mysql2": "^3.15.3", + "vitest": "^4.0.18", }, "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/bun": "latest", "@types/express": "^5.0.6", - "@types/mysql": "^2.15.21", + "@types/node": "^25.1.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "globals": "^17.3.0", + "jiti": "^2.6.1", + "typescript-eslint": "^8.54.0", + }, + "peerDependencies": { + "typescript": "^5.9.3", }, }, }, "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], + + "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@mysql/xdevapi": ["@mysql/xdevapi@8.0.35", "", { "dependencies": { "google-protobuf": "3.19.4", "lossless-json": "2.0.1", "parsimmon": "1.18.1" } }, "sha512-l7HoBt1l0GwdCBXBrqri4kPBbJrqsyvaa4eqXvq50aWWArOTc5XGV06FWF1epxT+RORBy7scZTWFANxpr4Sfuw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], @@ -39,26 +195,110 @@ "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -67,14 +307,30 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], @@ -83,40 +339,120 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "google-protobuf": ["google-protobuf@3.19.4", "", {}, "sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -127,20 +463,102 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], @@ -153,12 +571,20 @@ "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lossless-json": ["lossless-json@2.0.1", "", {}, "sha512-KW/FSL426qblKVvf4ImeMVGr0Je6J9aXvAMUOIU8AzelDj06q47mn6QJ+56lBBd+A8kjrncrxdKQs6ZssAXTmw=="], + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], @@ -169,48 +595,132 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mysql2": ["mysql2@3.18.2", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw=="], "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "parsimmon": ["parsimmon@1.18.1", "", {}, "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -219,24 +729,118 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/app/server/services/ts/user/package.json b/app/server/services/ts/user/package.json index 78c4daf..e407a4d 100644 --- a/app/server/services/ts/user/package.json +++ b/app/server/services/ts/user/package.json @@ -26,10 +26,11 @@ "start": "bun dist/index.js" }, "dependencies": { + "@mysql/xdevapi": "^8.0.35", "axios": "^1.13.6", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mysql2": "^3.15.3", - "@mysql/xdevapi": "^8.0.35" + "vitest": "^4.0.18" } } diff --git a/app/server/services/ts/user/src/logic/expenses.ts b/app/server/services/ts/user/src/logic/expenses.ts index 4b63401..6b4fac6 100644 --- a/app/server/services/ts/user/src/logic/expenses.ts +++ b/app/server/services/ts/user/src/logic/expenses.ts @@ -43,6 +43,7 @@ export async function getExpensesChartData( ); const allLabels = generateDateRange(startDate, endDate, period); + const dataMap = new Map(); expenses.forEach((row) => { diff --git a/app/server/services/ts/user/src/logic/transactions.ts b/app/server/services/ts/user/src/logic/transactions.ts index bf223ad..bb1e4c5 100644 --- a/app/server/services/ts/user/src/logic/transactions.ts +++ b/app/server/services/ts/user/src/logic/transactions.ts @@ -26,15 +26,3 @@ export async function getTransactionsData( return transactions; } - -export async function getTransactionsDataSafe( - pool: Pool, - userId: string, -): Promise { - try { - return await getTransactionsData(pool, userId); - } catch (error) { - console.error("Error in getTransactionsData:", error); - return null; - } -} diff --git a/app/server/services/ts/user/src/queries/snapshot.ts b/app/server/services/ts/user/src/queries/snapshot.ts index fd68a26..c01d6b2 100644 --- a/app/server/services/ts/user/src/queries/snapshot.ts +++ b/app/server/services/ts/user/src/queries/snapshot.ts @@ -10,59 +10,59 @@ export async function getSnapshotQuery( const [results] = await pool.query( ` SELECT - (SELECT COALESCE(SUM(fa.balance), 0) - FROM finus.financialAccount fa - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - WHERE uap.account_id = ?) as total_balance, - - (SELECT COALESCE(SUM(t.amount), 0) - FROM finus.transaction t - JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - WHERE uap.account_id = ? - AND t.amount > 0 - AND t.date BETWEEN ? AND ?) as current_income, - - (SELECT COALESCE(SUM(ABS(t.amount)), 0) - FROM finus.transaction t - JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - WHERE uap.account_id = ? - AND t.amount < 0 - AND t.date BETWEEN ? AND ?) as total_expenses, - - (SELECT COUNT(*) - FROM finus.transaction t - JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - WHERE uap.account_id = ? - AND t.amount < 0 - AND t.date BETWEEN ? AND ?) as transaction_count, - - (SELECT COALESCE(SUM(fa.balance), 0) - FROM finus.financialAccount fa - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - WHERE uap.account_id = ? - AND fa.type = 'credit_card' - AND fa.subtype = 'loan') as current_debt, - - (SELECT COALESCE(SUM(fa.balance), 0) - FROM finus.financialAccount fa - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - WHERE uap.account_id = ? - AND fa.type = 'savings') as total_savings + (SELECT COALESCE(SUM(fa.balance), 0) + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ?) as total_balance, + + (SELECT COALESCE(SUM(t.amount), 0) + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ? + AND t.amount > 0 + AND t.date BETWEEN ? AND ?) as current_income, + + (SELECT COALESCE(SUM(ABS(t.amount)), 0) + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ? + AND t.amount < 0 + AND t.date BETWEEN ? AND ?) as total_expenses, + + (SELECT COUNT(*) + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ? + AND t.amount < 0 + AND t.date BETWEEN ? AND ?) as transaction_count, + + (SELECT COALESCE(SUM(fa.balance), 0) + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ? + AND fa.type = 'credit_card' + AND fa.subtype = 'loan') as current_debt, + + (SELECT COALESCE(SUM(fa.balance), 0) + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ? + AND fa.type = 'savings') as total_savings `, [ userId, // total_balance diff --git a/app/server/services/ts/user/src/types/Transaction.ts b/app/server/services/ts/user/src/types/Transaction.ts index 9ea4e42..b637e05 100644 --- a/app/server/services/ts/user/src/types/Transaction.ts +++ b/app/server/services/ts/user/src/types/Transaction.ts @@ -2,7 +2,7 @@ import type { RowDataPacket } from "mysql2/promise"; export interface Transaction extends RowDataPacket { id: number; - financialAccount_id: string; + financialAccount_id: number; amount: number; category: string; description: string; diff --git a/app/server/services/ts/user/src/utils/dates.ts b/app/server/services/ts/user/src/utils/dates.ts index 7491f38..f75e550 100644 --- a/app/server/services/ts/user/src/utils/dates.ts +++ b/app/server/services/ts/user/src/utils/dates.ts @@ -3,6 +3,10 @@ export function generateDateRange( end: Date, period: string, ): string[] { + //check that dates are valid date objects + if (!start || !end || isNaN(start.getTime()) || isNaN(end.getTime())) { + throw new Error("Invalid start or end date"); + } const dates: string[] = []; const current = new Date(start); current.setHours(0, 0, 0, 0); diff --git a/app/server/services/ts/user/tests/dates.test.ts b/app/server/services/ts/user/tests/dates.test.ts new file mode 100644 index 0000000..5acb76f --- /dev/null +++ b/app/server/services/ts/user/tests/dates.test.ts @@ -0,0 +1,221 @@ +// tests/utils/dates.test.ts +import { describe, expect, it } from "vitest"; +import { generateDateRange } from "../src/utils/dates.ts"; +import { + createDailyDateRangeCases, + createMonthlyDateRangeCases, + createEdgeDateCases, +} from "./factories/date.factory.ts"; + +describe("generateDateRange", () => { + describe("Daily periods (week/month)", () => { + const dailyCases = createDailyDateRangeCases(); + + dailyCases.forEach(({ name, start, end, period, expected }) => { + it(`should handle ${name}`, () => { + const result = generateDateRange(start, end, period); + + expect(result).toEqual(expected); + }); + }); + + it("should handle dates with time components", () => { + const start = new Date("2024-03-09T14:30:00Z"); + const end = new Date("2024-03-15T09:15:00Z"); + const period = "w"; + const expected = [ + "2024-03-09", + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + ]; + + const result = generateDateRange(start, end, period); + + expect(result).toEqual(expected); + }); + + it("should normalize hours to midnight", () => { + const start = new Date("2024-03-09T23:59:59Z"); + const end = new Date("2024-03-10T00:00:01Z"); + const period = "w"; + const expected = ["2024-03-09", "2024-03-10"]; + + const result = generateDateRange(start, end, period); + + expect(result).toEqual(expected); + }); + }); + + describe("Monthly periods (year)", () => { + const monthlyCases = createMonthlyDateRangeCases(); + + monthlyCases.forEach(({ name, start, end, period, expected }) => { + it(`should handle ${name}`, () => { + // Act + const result = generateDateRange(start, end, period); + + // Assert + expect(result).toEqual(expected); + }); + }); + + it("should handle dates in middle of month", () => { + const start = new Date("2024-03-15T00:00:00Z"); + const end = new Date("2024-06-15T00:00:00Z"); + const period = "y"; + const expected = ["2024-03", "2024-04", "2024-05", "2024-06"]; + + const result = generateDateRange(start, end, period); + + expect(result).toEqual(expected); + }); + + it("should handle year boundaries", () => { + const start = new Date("2023-11-15T00:00:00Z"); + const end = new Date("2024-02-15T00:00:00Z"); + const period = "y"; + const expected = ["2023-11", "2023-12", "2024-01", "2024-02"]; + + const result = generateDateRange(start, end, period); + + expect(result).toEqual(expected); + }); + }); + + describe("Edge cases", () => { + const edgeCases = createEdgeDateCases(); + + edgeCases.forEach(({ name, start, end, period, expected }) => { + it(`should handle ${name}`, () => { + const result = generateDateRange(start, end, period); + + expect(result).toEqual(expected); + }); + }); + + it("should handle start date after end date", () => { + const start = new Date("2024-03-15"); + const end = new Date("2024-03-10"); + const period = "w"; + + const result = generateDateRange(start, end, period); + + expect(result).toEqual([]); + }); + + it("should handle very large date ranges", () => { + const start = new Date("2020-01-01"); + const end = new Date("2024-12-31"); + const period = "y"; + + const result = generateDateRange(start, end, period); + + expect(result).toHaveLength(60); + expect(result[0]).toBe("2020-01"); + expect(result[59]).toBe("2024-12"); + }); + + it("should handle invalid date objects", () => { + const start = new Date("invalid"); + const end = new Date("2024-03-15"); + const period = "w"; + + expect(() => generateDateRange(start, end, period)).toThrow(); + }); + + it("should preserve ordering when dates are reversed", () => { + const start = new Date("2024-03-15"); + const end = new Date("2024-03-10"); + const period = "w"; + + const result = generateDateRange(start, end, period); + + expect(result).toEqual([]); + }); + }); + + describe("Period-specific behavior", () => { + it("should handle weekly period with 7 days", () => { + const start = new Date("2024-03-10"); + const end = new Date("2024-03-16"); + const period = "w"; + const expected = [ + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + "2024-03-16", + ]; + + const result = generateDateRange(start, end, period); + + expect(result).toEqual(expected); + }); + + it("should handle monthly period with 30 days", () => { + const start = new Date("2024-03-01"); + const end = new Date("2024-03-30"); + const period = "m"; + const result = generateDateRange(start, end, period); + + expect(result).toHaveLength(30); + expect(result[0]).toBe("2024-03-01"); + expect(result[29]).toBe("2024-03-30"); + }); + + it("should handle monthly period with 31 days", () => { + const start = new Date("2024-01-01"); + const end = new Date("2024-01-31"); + const period = "m"; + + const result = generateDateRange(start, end, period); + + expect(result).toHaveLength(31); + }); + + it("should handle yearly period with 12 months", () => { + const start = new Date("2024-01-01"); + const end = new Date("2024-12-31"); + const period = "y"; + + const result = generateDateRange(start, end, period); + + expect(result).toHaveLength(12); + expect(result).toEqual([ + "2024-01", + "2024-02", + "2024-03", + "2024-04", + "2024-05", + "2024-06", + "2024-07", + "2024-08", + "2024-09", + "2024-10", + "2024-11", + "2024-12", + ]); + }); + }); + + describe("Performance", () => { + it("should handle large ranges efficiently", () => { + const start = new Date("2000-01-01"); + const end = new Date("2024-12-31"); + const period = "m"; + + const startTime = performance.now(); + const result = generateDateRange(start, end, period); + const endTime = performance.now(); + + expect(endTime - startTime).toBeLessThan(100); + expect(result.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/app/server/services/ts/user/tests/factories/date.factory.ts b/app/server/services/ts/user/tests/factories/date.factory.ts new file mode 100644 index 0000000..ca78298 --- /dev/null +++ b/app/server/services/ts/user/tests/factories/date.factory.ts @@ -0,0 +1,158 @@ +export interface DateRangeTestCase { + name: string; + start: Date; + end: Date; + period: "w" | "m" | "y"; + expected: string[]; +} + +export function createDailyDateRangeCases(): DateRangeTestCase[] { + //const baseDate = new Date('2024-03-15T00:00:00Z'); + + return [ + { + name: "weekly range - 7 days", + start: new Date("2024-03-09T00:00:00Z"), + end: new Date("2024-03-15T00:00:00Z"), + period: "w", + expected: [ + "2024-03-09", + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + ], + }, + { + name: "monthly range - 30 days", + start: new Date("2024-02-15T00:00:00Z"), + end: new Date("2024-03-15T00:00:00Z"), + period: "m", + expected: Array.from({ length: 30 }, (_, i) => { + const date = new Date("2024-02-15"); + date.setDate(date.getDate() + i); + return date.toISOString().slice(0, 10); + }), + }, + { + name: "range crossing month boundary", + start: new Date("2024-01-30T00:00:00Z"), + end: new Date("2024-02-05T00:00:00Z"), + period: "w", + expected: [ + "2024-01-30", + "2024-01-31", + "2024-02-01", + "2024-02-02", + "2024-02-03", + "2024-02-04", + "2024-02-05", + ], + }, + { + name: "range crossing year boundary", + start: new Date("2023-12-30T00:00:00Z"), + end: new Date("2024-01-05T00:00:00Z"), + period: "w", + expected: [ + "2023-12-30", + "2023-12-31", + "2024-01-01", + "2024-01-02", + "2024-01-03", + "2024-01-04", + "2024-01-05", + ], + }, + ]; +} + +export function createMonthlyDateRangeCases(): DateRangeTestCase[] { + return [ + { + name: "yearly range - 12 months", + start: new Date("2023-04-01T00:00:00Z"), + end: new Date("2024-03-01T00:00:00Z"), + period: "y", + expected: [ + "2023-04", + "2023-05", + "2023-06", + "2023-07", + "2023-08", + "2023-09", + "2023-10", + "2023-11", + "2023-12", + "2024-01", + "2024-02", + "2024-03", + ], + }, + { + name: "yearly range - partial year", + start: new Date("2024-01-01T00:00:00Z"), + end: new Date("2024-06-01T00:00:00Z"), + period: "y", + expected: [ + "2024-01", + "2024-02", + "2024-03", + "2024-04", + "2024-05", + "2024-06", + ], + }, + { + name: "single month range", + start: new Date("2024-03-01T00:00:00Z"), + end: new Date("2024-03-01T00:00:00Z"), + period: "y", + expected: ["2024-03"], + }, + ]; +} + +export function createEdgeDateCases(): DateRangeTestCase[] { + return [ + { + name: "leap year February", + start: new Date("2024-02-28T00:00:00Z"), + end: new Date("2024-03-02T00:00:00Z"), + period: "w", + expected: ["2024-02-28", "2024-02-29", "2024-03-01", "2024-03-02"], + }, + { + name: "non-leap year February", + start: new Date("2023-02-27T00:00:00Z"), + end: new Date("2023-03-02T00:00:00Z"), + period: "w", + expected: ["2023-02-27", "2023-02-28", "2023-03-01", "2023-03-02"], + }, + { + name: "DST transition (should not affect dates)", + start: new Date("2024-03-09T00:00:00Z"), + end: new Date("2024-03-16T00:00:00Z"), + period: "w", + expected: [ + "2024-03-09", + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + "2024-03-16", + ], + }, + { + name: "same day range", + start: new Date("2024-03-15T10:30:00Z"), + end: new Date("2024-03-15T15:45:00Z"), + period: "w", + expected: ["2024-03-15"], + }, + ]; +} diff --git a/app/server/services/ts/user/tests/factories/expense.factory.ts b/app/server/services/ts/user/tests/factories/expense.factory.ts new file mode 100644 index 0000000..26a67f6 --- /dev/null +++ b/app/server/services/ts/user/tests/factories/expense.factory.ts @@ -0,0 +1,65 @@ +import type { ExpenseRow } from "../../src/types/ExpenseRow.ts"; + +export function createMockExpenseRow( + overrides?: Partial, +): ExpenseRow { + return { + label: "2022-01-01", + total_expenses: 100, + date_group: "2022-01-01", + ...overrides, + }; +} + +export function createMockExpenseRows(count: number = 5): ExpenseRow[] { + const rows: ExpenseRow[] = []; + const baseDate = new Date("2024-01-01"); + + for (let i = 0; i < count; i++) { + const date = new Date(baseDate); + date.setDate(date.getDate() + i); + const dateStr = date.toISOString().slice(0, 10); + + rows.push( + createMockExpenseRow({ + label: dateStr, + total_expenses: 100 + i * 50, + date_group: dateStr, + }), + ); + } + + return rows; +} + +export function createMockExpenseRowsWithLabels( + labels: string[], +): ExpenseRow[] { + return labels.map((label, index) => ({ + label, + total_expenses: 100 + index * 50, + date_group: label, + })); +} + +export function createEmptyExpenseRows(): ExpenseRow[] { + return []; +} + +export function createExpenseRowsWithGaps(): ExpenseRow[] { + return [ + { label: "2024-03-09", total_expenses: 100, date_group: "2024-03-09" }, + { label: "2024-03-11", total_expenses: 200, date_group: "2024-03-11" }, + { label: "2024-03-13", total_expenses: 150, date_group: "2024-03-13" }, + ]; +} + +export function createMonthlyExpenseRows(): ExpenseRow[] { + return [ + createMockExpenseRow({ label: "2024-01", total_expenses: 1000 }), + createMockExpenseRow({ label: "2024-02", total_expenses: 1200 }), + createMockExpenseRow({ label: "2024-03", total_expenses: 900 }), + createMockExpenseRow({ label: "2024-04", total_expenses: 1100 }), + createMockExpenseRow({ label: "2024-05", total_expenses: 1300 }), + ]; +} diff --git a/app/server/services/ts/user/tests/factories/transaction.factory.ts b/app/server/services/ts/user/tests/factories/transaction.factory.ts new file mode 100644 index 0000000..8e39961 --- /dev/null +++ b/app/server/services/ts/user/tests/factories/transaction.factory.ts @@ -0,0 +1,49 @@ +import type { Transaction } from "../../src/types/Transaction.ts"; + +export function createMockTransaction( + overrides?: Partial, +): Transaction { + // base values plus the required `constructor` property + return { + id: 1, + financialAccount_id: 1, + amount: 100, + category: "rent", + description: "test", + sender: "John Doe", + recipient: "Landlord", + date: "2022-01-01", + constructor: { name: "RowDataPacket" }, + ...overrides, + } as Transaction; +} + +export function createMockTransactions(count: number = 5): Transaction[] { + return Array.from({ length: count }, (_, i) => + createMockTransaction({ + id: i + 1, + financialAccount_id: i + 1, + amount: i % 2 === 0 ? -50 : 100, + category: i % 2 === 0 ? "rent" : "salary", + description: "test", + sender: "test", + recipient: "test", + date: "2022-01-01", + constructor: { name: "RowDataPacket" }, + }), + ); +} + +export function createEmptyTransactions(): Transaction[] { + return []; +} + +export function createTransactionsWithMissingFields(): Transaction[] { + return [ + createMockTransaction({ + sender: "", + recipient: "", + constructor: { name: "RowDataPacket" }, + }), + ]; +} diff --git a/app/server/services/ts/user/tests/logic_expenses.test.ts b/app/server/services/ts/user/tests/logic_expenses.test.ts new file mode 100644 index 0000000..e82ea11 --- /dev/null +++ b/app/server/services/ts/user/tests/logic_expenses.test.ts @@ -0,0 +1,338 @@ +import { vi, describe, expect, it, beforeEach, afterEach } from "vitest"; +import { getExpensesChartData } from "../src/logic/expenses.ts"; +import * as expensesQueries from "../src/queries/expenses.ts"; +import * as dates from "../src/utils/dates.ts"; +import type { Pool } from "mysql2/promise"; +import { + createMockExpenseRows, + createEmptyExpenseRows, + createExpenseRowsWithGaps, + createMonthlyExpenseRows, + createMockExpenseRowsWithLabels, +} from "./factories/expense.factory.ts"; + +vi.mock("../src/queries/expenses", () => ({ + getExpensesQuery: vi.fn(), +})); +vi.mock("../src/utils/dates", () => ({ + generateDateRange: vi.fn(), +})); + +describe("getExpensesChartData", () => { + const mockPool = {} as Pool; + const userId = "123"; + const mockGetExpensesQuery = expensesQueries.getExpensesQuery as vi.Mock; + const mockGenerateDateRange = dates.generateDateRange as vi.Mock; + + const mockDate = new Date("2024-03-15T12:00:00Z"); + const RealDate = Date; + + beforeEach(() => { + vi.resetAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime?.(mockDate); + //this tedious hack is needed to mock Date.now() + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + mockGenerateDateRange.mockImplementation(() => { + return []; + }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe("Period handling", () => { + it("should handle weekly period correctly", async () => { + const period = "w"; + const mockExpenses = createMockExpenseRows(7); + const expectedLabels = [ + "2024-03-09", + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + ]; + + mockGetExpensesQuery.mockResolvedValue(mockExpenses); + mockGenerateDateRange.mockReturnValue(expectedLabels); + + const result = await getExpensesChartData(mockPool, userId, period); + + expect(result.labels).toEqual(expectedLabels); + expect(result.datasets[0].label).toBe("Weekly Expenses"); + expect(result.datasets[0].data).toHaveLength(7); + + expect(mockGetExpensesQuery).toHaveBeenCalledWith( + mockPool, + userId, + "%Y-%m-%d", + expect.any(String), + expect.any(String), + "DATE(date)", + ); + }); + + it("should handle monthly period correctly", async () => { + const period = "m"; + const mockExpenses = createMockExpenseRows(30); + const expectedLabels = Array.from({ length: 30 }, (_, i) => { + const date = new Date(mockDate); + date.setDate(date.getDate() - (29 - i)); + return date.toISOString().slice(0, 10); + }); + + mockGetExpensesQuery.mockResolvedValue(mockExpenses); + mockGenerateDateRange.mockReturnValue(expectedLabels); + + const result = await getExpensesChartData(mockPool, userId, period); + + expect(result.labels).toEqual(expectedLabels); + expect(result.datasets[0].label).toBe("Monthly Expenses"); + expect(result.datasets[0].data).toHaveLength(30); + expect(mockGetExpensesQuery).toHaveBeenCalledWith( + mockPool, + userId, + "%Y-%m-%d", + expect.any(String), + expect.any(String), + "DATE(date)", + ); + }); + + it("should handle yearly period correctly", async () => { + const period = "y"; + const mockExpenses = createMonthlyExpenseRows(); + const expectedLabels = [ + "2023-04", + "2023-05", + "2023-06", + "2023-07", + "2023-08", + "2023-09", + "2023-10", + "2023-11", + "2023-12", + "2024-01", + "2024-02", + "2024-03", + ]; + + mockGetExpensesQuery.mockResolvedValue(mockExpenses); + mockGenerateDateRange.mockReturnValue(expectedLabels); + + const result = await getExpensesChartData(mockPool, userId, period); + + expect(result.labels).toEqual(expectedLabels); + expect(result.datasets[0].label).toBe("Yearly Expenses"); + expect(result.datasets[0].data).toHaveLength(12); + expect(mockGetExpensesQuery).toHaveBeenCalledWith( + mockPool, + userId, + "%Y-%m", + expect.any(String), + expect.any(String), + 'DATE_FORMAT(date, "%Y-%m-01")', + ); + }); + + it("should throw error for invalid period", async () => { + const invalidPeriod = "invalid"; + await expect( + getExpensesChartData(mockPool, userId, invalidPeriod), + ).rejects.toThrow("Invalid period"); + }); + }); + + describe("Data mapping", () => { + it("should map expense data correctly to labels", async () => { + const period = "w"; + const expectedLabels = [ + "2024-03-09", + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + ]; + + // Create mock expenses that match these labels + const mockExpenses = createMockExpenseRowsWithLabels(expectedLabels); + const expectedData = mockExpenses.map((e) => e.total_expenses); + + mockGetExpensesQuery.mockResolvedValue(mockExpenses); + mockGenerateDateRange.mockReturnValue(expectedLabels); + + const result = await getExpensesChartData(mockPool, userId, period); + + expect(result.datasets[0].data).toEqual(expectedData); + }); + + it("should fill zeros for missing dates", async () => { + const period = "w"; + const expectedLabels = [ + "2024-03-09", + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + ]; + + // Create expenses with gaps (missing 3-10, 3-12, 3-14) + const mockExpenses = createExpenseRowsWithGaps(); + + mockGetExpensesQuery.mockResolvedValue(mockExpenses); + mockGenerateDateRange.mockReturnValue(expectedLabels); + + const result = await getExpensesChartData(mockPool, userId, period); + + expect(result.datasets[0].data).toEqual([100, 0, 200, 0, 150, 0, 0]); + }); + + it("should handle empty expense data", async () => { + const period = "w"; + const mockExpenses = createEmptyExpenseRows(); + const expectedLabels = [ + "2024-03-09", + "2024-03-10", + "2024-03-11", + "2024-03-12", + "2024-03-13", + "2024-03-14", + "2024-03-15", + ]; + + mockGetExpensesQuery.mockResolvedValue(mockExpenses); + mockGenerateDateRange.mockReturnValue(expectedLabels); + + const result = await getExpensesChartData(mockPool, userId, period); + + expect(result.datasets[0].data).toEqual([0, 0, 0, 0, 0, 0, 0]); + }); + }); + + describe("Date range calculation", () => { + it("should calculate correct date range for weekly period", async () => { + const period = "w"; + mockGetExpensesQuery.mockResolvedValue([]); + await getExpensesChartData(mockPool, userId, period); + + expect(mockGenerateDateRange).toHaveBeenCalled(); + const call = mockGenerateDateRange.mock.calls[0]; + expect(call).toBeDefined(); + const [startDate, endDate, calledPeriod] = call!; + + // Verify start date is 7 days ago + const expectedStartDate = new Date(mockDate); + expectedStartDate.setDate(expectedStartDate.getDate() - 7); + expect(startDate.toISOString().slice(0, 10)).toBe( + expectedStartDate.toISOString().slice(0, 10), + ); + expect(endDate.toISOString().slice(0, 10)).toBe( + mockDate.toISOString().slice(0, 10), + ); + expect(calledPeriod).toBe(period); + }); + + it("should calculate correct date range for monthly period", async () => { + const period = "m"; + mockGetExpensesQuery.mockResolvedValue([]); + + await getExpensesChartData(mockPool, userId, period); + + expect(mockGenerateDateRange).toHaveBeenCalled(); + const call = mockGenerateDateRange.mock.calls[0]; + expect(call).toBeDefined(); + const [startDate, endDate, calledPeriod] = call!; + + //verify start date is 30 days ago + const expectedStartDate = new Date(mockDate); + expectedStartDate.setDate(expectedStartDate.getDate() - 30); + expect(startDate.toISOString().slice(0, 10)).toBe( + expectedStartDate.toISOString().slice(0, 10), + ); + expect(endDate.toISOString().slice(0, 10)).toBe( + mockDate.toISOString().slice(0, 10), + ); + expect(calledPeriod).toBe(period); + }); + + //if this does not fail, then it passes + it("should calculate correct date range for yearly period", async () => { + const period = "y"; + mockGetExpensesQuery.mockResolvedValue([]); + + await getExpensesChartData(mockPool, userId, period); + + expect(mockGenerateDateRange).toHaveBeenCalled(); + const call = mockGenerateDateRange.mock.calls[0]; + expect(call).toBeDefined(); + const [startDate, endDate, calledPeriod] = call!; + + //verify start date is 365 days ago + const expectedStartDate = new Date(mockDate); + expectedStartDate.setDate(expectedStartDate.getDate() - 365); + expect(startDate.toISOString().slice(0, 10)).toBe( + expectedStartDate.toISOString().slice(0, 10), + ); + expect(endDate.toISOString().slice(0, 10)).toBe( + mockDate.toISOString().slice(0, 10), + ); + expect(calledPeriod).toBe(period); + }); + }); + + describe("Query parameter handling", () => { + it("should pass correct parameters to getExpensesQuery", async () => { + const period = "w"; + mockGetExpensesQuery.mockResolvedValue([]); + mockGenerateDateRange.mockReturnValue([]); + + await getExpensesChartData(mockPool, userId, period); + + expect(mockGetExpensesQuery).toHaveBeenCalledWith( + mockPool, + userId, + "%Y-%m-%d", + expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), + expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), + "DATE(date)", + ); + }); + + it("should use different date format for yearly period", async () => { + const period = "y"; + mockGetExpensesQuery.mockResolvedValue([]); + mockGenerateDateRange.mockReturnValue([]); + + await getExpensesChartData(mockPool, userId, period); + + expect(mockGetExpensesQuery).toHaveBeenCalledWith( + mockPool, + userId, + "%Y-%m", + expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), + expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), + 'DATE_FORMAT(date, "%Y-%m-01")', + ); + }); + }); +}); diff --git a/app/server/services/ts/user/tests/logic_snapshot.test.ts b/app/server/services/ts/user/tests/logic_snapshot.test.ts new file mode 100644 index 0000000..46b36ed --- /dev/null +++ b/app/server/services/ts/user/tests/logic_snapshot.test.ts @@ -0,0 +1,441 @@ +// tests/logic/snapshot.test.ts +import { vi, describe, expect, it, beforeEach, afterEach } from "vitest"; +import type { Pool } from "mysql2/promise"; +import { getSnapshotData, getSnapshotDataSafe } from "../src/logic/snapshot.ts"; +import * as snapshotQueries from "../src/queries/snapshot.ts"; +import type { SnapshotRow } from "../src/types/SnapshotRow.ts"; + +// Mock the queries module +vi.mock("../src/queries/snapshot", () => ({ + getSnapshotQuery: vi.fn(), +})); + +describe("getSnapshotData", () => { + const mockPool = {} as Pool; + const userId = "123"; + + const mockGetSnapshotQuery = snapshotQueries.getSnapshotQuery as vi.Mock; + + const RealDate = global.Date; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + global.Date = RealDate; + }); + + describe("Happy path", () => { + it("should return correctly formatted snapshot data for June", async () => { + // Arrange - Mock date to June 15, 2024 + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: 10000, + current_income: 5000, + total_expenses: 3000, + current_debt: 2000, + total_savings: 4000, + }, + ]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + + const result = await getSnapshotData(mockPool, userId); + expect(result).toEqual({ + totalBalance: 10000, + currentIncome: 5000, + averageExpenses: 500, + currentDebt: 2000, + totalSavings: 4000, + }); + expect(mockGetSnapshotQuery).toHaveBeenCalledWith( + mockPool, + userId, + "2024-01-01", + "2024-06-15", + ); + }); + + it("should return correctly formatted snapshot data for March", async () => { + const mockDate = new Date("2024-03-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: 10000, + current_income: 5000, + total_expenses: 1500, + current_debt: 2000, + total_savings: 4000, + }, + ]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + + const result = await getSnapshotData(mockPool, userId); + + expect(result).toEqual({ + totalBalance: 10000, + currentIncome: 5000, + averageExpenses: 500, + currentDebt: 2000, + totalSavings: 4000, + }); + + expect(mockGetSnapshotQuery).toHaveBeenCalledWith( + mockPool, + userId, + "2024-01-01", + "2024-03-15", + ); + }); + + it("should return correctly formatted snapshot data for December", async () => { + const mockDate = new Date("2024-12-31T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: 10000, + current_income: 5000, + total_expenses: 6000, + current_debt: 2000, + total_savings: 4000, + }, + ]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + + const result = await getSnapshotData(mockPool, userId); + expect(result).toEqual({ + totalBalance: 10000, + currentIncome: 5000, + averageExpenses: 500, + currentDebt: 2000, + totalSavings: 4000, + }); + + expect(mockGetSnapshotQuery).toHaveBeenCalledWith( + mockPool, + userId, + "2024-01-01", + "2024-12-31", + ); + }); + }); + + describe("Edge cases", () => { + it("should throw error when no snapshot data found", async () => { + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + mockGetSnapshotQuery.mockResolvedValue([]); + + await expect(getSnapshotData(mockPool, userId)).rejects.toThrow( + "No snapshot data found", + ); + }); + + it("should handle zero values correctly", async () => { + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: 0, + current_income: 0, + total_expenses: 0, + current_debt: 0, + total_savings: 0, + }, + ]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + + const result = await getSnapshotData(mockPool, userId); + + expect(result).toEqual({ + totalBalance: 0, + currentIncome: 0, + averageExpenses: 0, + currentDebt: 0, + totalSavings: 0, + }); + }); + + it("should handle missing fields by defaulting to 0", async () => { + // Arrange + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: 10000, + total_expenses: 3000, + total_savings: 4000, + }, + ] as SnapshotRow[]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + + const result = await getSnapshotData(mockPool, userId); + + expect(result).toEqual({ + totalBalance: 10000, + currentIncome: 0, + averageExpenses: 500, + currentDebt: 0, + totalSavings: 4000, + }); + }); + + it("should handle negative values correctly", async () => { + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: -1000, + current_income: 5000, + total_expenses: 3000, + current_debt: 5000, + total_savings: -500, + }, + ]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + + const result = await getSnapshotData(mockPool, userId); + + expect(result).toEqual({ + totalBalance: -1000, + currentIncome: 5000, + averageExpenses: 500, + currentDebt: 5000, + totalSavings: -500, + }); + }); + + it("should handle monthsPassed = 0 (January 1st)", async () => { + const mockDate = new Date("2024-01-01T00:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: 10000, + current_income: 0, + total_expenses: 0, + current_debt: 2000, + total_savings: 4000, + }, + ]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + + const result = await getSnapshotData(mockPool, userId); + + expect(result.averageExpenses).toBe(0); // monthsPassed = 0, so avg = 0 + expect(mockGetSnapshotQuery).toHaveBeenCalledWith( + mockPool, + userId, + "2024-01-01", + "2024-01-01", + ); + }); + }); + + describe("getSnapshotDataSafe", () => { + it("should return data on success", async () => { + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + const mockQueryResult = [ + { + total_balance: 10000, + current_income: 5000, + total_expenses: 3000, + current_debt: 2000, + total_savings: 4000, + }, + ]; + + mockGetSnapshotQuery.mockResolvedValue(mockQueryResult); + const result = await getSnapshotDataSafe(mockPool, userId); + + expect(result).toEqual({ + totalBalance: 10000, + currentIncome: 5000, + averageExpenses: 500, + currentDebt: 2000, + totalSavings: 4000, + }); + }); + + it("should return null when no data found", async () => { + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + + mockGetSnapshotQuery.mockResolvedValue([]); + let result = null; + try { + result = await getSnapshotDataSafe(mockPool, userId); + } catch (error) { + expect(error).toBe("No snapshot data found"); + } + expect(result).toBeNull(); + }); + + //have to spy here because error can escape the tests and break the suite + it("should return null on database error", async () => { + const mockDate = new Date("2024-06-15T12:00:00Z"); + global.Date = class extends RealDate { + constructor(...args: unknown[]) { + if (args.length === 0) { + super(mockDate); + } else { + super(...(args as ConstructorParameters)); + } + } + static now() { + return mockDate.getTime(); + } + } as DateConstructor; + const consoleSpy = vi + .spyOn(console, "error") + .mockImplementation(() => {}); + + const dbError = new Error("DB Error"); + mockGetSnapshotQuery.mockRejectedValue(dbError); + + let result = null; + try { + result = await getSnapshotDataSafe(mockPool, userId); + } catch (error) { + expect(error).toBe(dbError); + } + //const result = await getSnapshotDataSafe(mockPool, userId); + + expect(result).toBeNull(); + expect(consoleSpy).toHaveBeenCalledWith( + "Error in getSnapshotData:", + dbError, + ); + + consoleSpy.mockRestore(); + }); + }); +}); diff --git a/app/server/services/ts/user/tests/logic_transactions.test.ts b/app/server/services/ts/user/tests/logic_transactions.test.ts new file mode 100644 index 0000000..0eb7c26 --- /dev/null +++ b/app/server/services/ts/user/tests/logic_transactions.test.ts @@ -0,0 +1,87 @@ +import { vi, describe, expect, it, beforeEach } from "vitest"; +import { getTransactionsData } from "../src/logic/transactions.ts"; +import * as queries from "../src/queries/transactions.ts"; +import type { Pool } from "mysql2/promise"; +import { + createMockTransactions, + createMockTransaction, + createEmptyTransactions, + createTransactionsWithMissingFields, +} from "./factories/transaction.factory.ts"; + +vi.mock("../src/queries/transactions", () => ({ + getAllTransactionsQuery: vi.fn(), +})); + +describe("getTransactionsData", () => { + const mockPool = {} as Pool; + const userId = "123"; + + const mockGetAllTransactionsQuery = + queries.getAllTransactionsQuery as vi.Mock; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe("Happy path", () => { + it("should return enriched transactions", async () => { + const mockTransactions = createMockTransactions(3); + mockGetAllTransactionsQuery.mockResolvedValue(mockTransactions); + + const result = await getTransactionsData(mockPool, userId); + + expect(result).toHaveLength(3); + expect(result[0].sender).toBeDefined(); + expect(result[0].recipient).toBeDefined(); + expect(mockGetAllTransactionsQuery).toHaveBeenCalledWith( + mockPool, + userId, + ); + }); + + it("should preserve existing sender/recipient fields", async () => { + const mockTransactions = [ + createMockTransaction({ + sender: "Custom Sender", + recipient: "Custom Recipient", + constructor: { name: "RowDataPacket" }, + }), + ]; + mockGetAllTransactionsQuery.mockResolvedValue(mockTransactions); + + const result = await getTransactionsData(mockPool, userId); + + expect(result[0].sender).toBe("Custom Sender"); + expect(result[0].recipient).toBe("Custom Recipient"); + }); + }); + + describe("Edge cases", () => { + it("should handle empty transactions", async () => { + mockGetAllTransactionsQuery.mockResolvedValue(createEmptyTransactions()); + + const result = await getTransactionsData(mockPool, userId); + + expect(result).toEqual([]); + }); + + it("should handle missing sender/recipient fields", async () => { + const mockTransactions = createTransactionsWithMissingFields(); + mockGetAllTransactionsQuery.mockResolvedValue(mockTransactions); + + const result = await getTransactionsData(mockPool, userId); + + expect(result[0].sender).toBeTruthy(); + expect(result[0].recipient).toBeTruthy(); + }); + + it("should handle database errors gracefully", async () => { + mockGetAllTransactionsQuery.mockRejectedValue(new Error("DB Error")); + + await expect(getTransactionsData(mockPool, userId)).rejects.toThrow( + "DB Error", + ); + }); + }); +}); From 8a3924f5289531c689756bea56f80c2df5dc8ff7 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 07:32:15 -0600 Subject: [PATCH 161/204] renaming some files for consistency --- app/server/services/ts/user/{tests => test}/dates.test.ts | 0 .../services/ts/user/{tests => test}/factories/date.factory.ts | 0 .../services/ts/user/{tests => test}/factories/expense.factory.ts | 0 .../ts/user/{tests => test}/factories/transaction.factory.ts | 0 .../services/ts/user/{tests => test}/logic_expenses.test.ts | 0 .../services/ts/user/{tests => test}/logic_snapshot.test.ts | 0 .../services/ts/user/{tests => test}/logic_transactions.test.ts | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename app/server/services/ts/user/{tests => test}/dates.test.ts (100%) rename app/server/services/ts/user/{tests => test}/factories/date.factory.ts (100%) rename app/server/services/ts/user/{tests => test}/factories/expense.factory.ts (100%) rename app/server/services/ts/user/{tests => test}/factories/transaction.factory.ts (100%) rename app/server/services/ts/user/{tests => test}/logic_expenses.test.ts (100%) rename app/server/services/ts/user/{tests => test}/logic_snapshot.test.ts (100%) rename app/server/services/ts/user/{tests => test}/logic_transactions.test.ts (100%) diff --git a/app/server/services/ts/user/tests/dates.test.ts b/app/server/services/ts/user/test/dates.test.ts similarity index 100% rename from app/server/services/ts/user/tests/dates.test.ts rename to app/server/services/ts/user/test/dates.test.ts diff --git a/app/server/services/ts/user/tests/factories/date.factory.ts b/app/server/services/ts/user/test/factories/date.factory.ts similarity index 100% rename from app/server/services/ts/user/tests/factories/date.factory.ts rename to app/server/services/ts/user/test/factories/date.factory.ts diff --git a/app/server/services/ts/user/tests/factories/expense.factory.ts b/app/server/services/ts/user/test/factories/expense.factory.ts similarity index 100% rename from app/server/services/ts/user/tests/factories/expense.factory.ts rename to app/server/services/ts/user/test/factories/expense.factory.ts diff --git a/app/server/services/ts/user/tests/factories/transaction.factory.ts b/app/server/services/ts/user/test/factories/transaction.factory.ts similarity index 100% rename from app/server/services/ts/user/tests/factories/transaction.factory.ts rename to app/server/services/ts/user/test/factories/transaction.factory.ts diff --git a/app/server/services/ts/user/tests/logic_expenses.test.ts b/app/server/services/ts/user/test/logic_expenses.test.ts similarity index 100% rename from app/server/services/ts/user/tests/logic_expenses.test.ts rename to app/server/services/ts/user/test/logic_expenses.test.ts diff --git a/app/server/services/ts/user/tests/logic_snapshot.test.ts b/app/server/services/ts/user/test/logic_snapshot.test.ts similarity index 100% rename from app/server/services/ts/user/tests/logic_snapshot.test.ts rename to app/server/services/ts/user/test/logic_snapshot.test.ts diff --git a/app/server/services/ts/user/tests/logic_transactions.test.ts b/app/server/services/ts/user/test/logic_transactions.test.ts similarity index 100% rename from app/server/services/ts/user/tests/logic_transactions.test.ts rename to app/server/services/ts/user/test/logic_transactions.test.ts From f3d3a3b0db60b924430f0815910198053650e12e Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 07:40:50 -0600 Subject: [PATCH 162/204] got rid of fake timers in user-service tests --- app/server/services/ts/user/test/logic_expenses.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/server/services/ts/user/test/logic_expenses.test.ts b/app/server/services/ts/user/test/logic_expenses.test.ts index e82ea11..b9eac88 100644 --- a/app/server/services/ts/user/test/logic_expenses.test.ts +++ b/app/server/services/ts/user/test/logic_expenses.test.ts @@ -25,12 +25,14 @@ describe("getExpensesChartData", () => { const mockGenerateDateRange = dates.generateDateRange as vi.Mock; const mockDate = new Date("2024-03-15T12:00:00Z"); - const RealDate = Date; + const RealDate = global.Date; beforeEach(() => { vi.resetAllMocks(); - vi.useFakeTimers(); - vi.setSystemTime?.(mockDate); + //GitHub actions does not support fake timers for some reason so the tests just rely on the mocked Date + //vi.useFakeTimers(); + //vi.setSystemTime?.(mockDate); + //this tedious hack is needed to mock Date.now() global.Date = class extends RealDate { constructor(...args: unknown[]) { From 663b589e722d3c3024ce0985394b3d508510a3e0 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 08:04:38 -0600 Subject: [PATCH 163/204] Updated current unit tests and added unit tests for validate row and handle input --- app/client/test/handleInput.test.ts | 92 +++++++++++++++++ app/client/test/parseCsvFile.test.ts | 2 +- app/client/test/validateFile.test.ts | 7 +- app/client/test/validateForms.test.ts | 140 ++++++++++++++++++++++++++ app/client/test/validateRow.test.ts | 2 +- 5 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 app/client/test/handleInput.test.ts create mode 100644 app/client/test/validateForms.test.ts diff --git a/app/client/test/handleInput.test.ts b/app/client/test/handleInput.test.ts new file mode 100644 index 0000000..cc52883 --- /dev/null +++ b/app/client/test/handleInput.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi } from "vitest"; +import { + handleCurrencyChange, + handleCurrencyBlur, +} from "../src/utils/handleInput"; +//tests for handleCurrencyChange and handleCurrencyBlur functions +function mockEvent(value: string) { + return { + target: { value }, + } as React.ChangeEvent; +} + +describe("handleCurrencyChange", () => { + it("accepts valid currency input", () => { + const setCurrency = vi.fn(); + const event = mockEvent("12.34"); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).toHaveBeenCalledWith("12.34"); + }); + + it("rejects invalid currency input", () => { + const setCurrency = vi.fn(); + const event = mockEvent("12.345"); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).not.toHaveBeenCalled(); + }); + + it("empty string", () => { + const setCurrency = vi.fn(); + const event = mockEvent(""); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).toHaveBeenCalledWith(""); + }); + + it("strips leading zeros", () => { + const setCurrency = vi.fn(); + const event = mockEvent("00045.20"); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).toHaveBeenCalledWith("45.20"); + }); + + it("accepts partial decimal input like '12.' or '12.3'", () => { + const setCurrency = vi.fn(); + const event = mockEvent("12."); + + handleCurrencyChange(event, setCurrency); + expect(setCurrency).toHaveBeenCalledWith("12."); + + const setCurrency2 = vi.fn(); + const event2 = mockEvent("12.3"); + + handleCurrencyChange(event2, setCurrency2); + expect(setCurrency2).toHaveBeenCalledWith("12.3"); + }); +}); + +describe("handleCurrencyBlur", () => { + it("formats to two decimals on blur", () => { + const setCurrency = vi.fn(); + const event = mockEvent("12.3"); + + handleCurrencyBlur(event, "12.3", setCurrency); + + expect(setCurrency).toHaveBeenCalledWith("12.30"); + }); + + it("does nothing when input is empty", () => { + const setCurrency = vi.fn(); + const event = mockEvent(""); + + handleCurrencyBlur(event, "", setCurrency); + + expect(setCurrency).not.toHaveBeenCalled(); + }); + + it("formats integer values to two decimals", () => { + const setCurrency = vi.fn(); + const event = mockEvent("45"); + + handleCurrencyBlur(event, "45", setCurrency); + + expect(setCurrency).toHaveBeenCalledWith("45.00"); + }); +}); diff --git a/app/client/test/parseCsvFile.test.ts b/app/client/test/parseCsvFile.test.ts index 5477152..eebe6ca 100644 --- a/app/client/test/parseCsvFile.test.ts +++ b/app/client/test/parseCsvFile.test.ts @@ -9,7 +9,7 @@ import Papa, { import { normalizeRow } from "../src/utils/NormalizeRow"; import { convertDraft } from "../src/utils/ConvertTransaction"; -// Extend PapaParse types to match real runtime behavior +// Extend PapaParse types to match real runtime behavior, this ensures our mocks are correctly typed and we can call the complete and error callbacks in our tests without type issues. type FixedParseConfig = ParseConfig & { error?: (err: Error, file?: File) => void; complete?: (results: ParseResult, file?: File) => void; diff --git a/app/client/test/validateFile.test.ts b/app/client/test/validateFile.test.ts index 7913483..3f2e904 100644 --- a/app/client/test/validateFile.test.ts +++ b/app/client/test/validateFile.test.ts @@ -32,7 +32,7 @@ describe("validateFile", () => { tooManyInvalid: true, // 100% invalid }); }); - + //here we check the case where 20% or more of rows are invalid it("detects when more than 20% of rows are invalid", () => { const rows: TransactionDraft[] = [ { date: "2024-01-01", description: "A", amount: 10, errors: [] }, @@ -44,9 +44,10 @@ describe("validateFile", () => { expect(result.validCount).toBe(2); expect(result.invalidCount).toBe(1); - expect(result.tooManyInvalid).toBe(true); // more then 20% invalid + expect(result.tooManyInvalid).toBe(true); }); + //here we check the case where exactly 20% of rows are invalid it("does not flag tooManyInvalid when invalid rows are 20% or less", () => { const rows: TransactionDraft[] = [ { date: "2024-01-01", description: "A", amount: 10, errors: [] }, @@ -60,7 +61,7 @@ describe("validateFile", () => { expect(result.validCount).toBe(4); expect(result.invalidCount).toBe(1); - expect(result.tooManyInvalid).toBe(false); // 1/5 = 20% + expect(result.tooManyInvalid).toBe(false); }); it("handles an empty file", () => { diff --git a/app/client/test/validateForms.test.ts b/app/client/test/validateForms.test.ts new file mode 100644 index 0000000..5633443 --- /dev/null +++ b/app/client/test/validateForms.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect } from "vitest"; +import { + validateAccountForm, + validateTransactionForm, + validateIncomeForm, +} from "../src/utils/ValidateForms"; +import { accountCategory } from "../src/enum/AccountCategory"; +import { transactionCategory } from "../src/enum/TransactionCategory"; +//tests for validateForms functions +function mockFile(): File { + return new File(["dummy"], "test.csv", { type: "text/csv" }); +} + +describe("validateAccountForm", () => { + it("returns true when CSV file is provided (CSV overrides all other checks)", () => { + const file = mockFile(); + const result = validateAccountForm("My Account", "Checking", 100, file); + expect(result).toBe(true); + }); + + it("returns true for valid manual account input", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm("My Account", validType, 500); + expect(result).toBe(true); + }); + + it("returns false when name is missing", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm("", validType, 500); + expect(result).toBe(false); + }); + + it("returns false for invalid account type", () => { + const result = validateAccountForm("My Account", "INVALID_TYPE", 100); + expect(result).toBe(false); + }); + + it("returns false for negative balance", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm("My Account", validType, -10); + expect(result).toBe(false); + }); + + it("returns false for invalid interest rate", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm( + "My Account", + validType, + 100, + undefined, + undefined, + 200, + ); + expect(result).toBe(false); + }); + + it("returns true when optional subtype and interest are valid", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm( + "My Account", + validType, + 100, + undefined, + "sub", + 5, + ); + expect(result).toBe(true); + }); +}); + +describe("validateTransactionForm", () => { + it("returns true when CSV file is provided", () => { + const file = mockFile(); + const result = validateTransactionForm( + "123", + "Deposit", + 100, + "2024-01-01", + file, + ); + expect(result).toBe(true); + }); + + it("returns true for valid manual transaction input", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm("123", validType, 50, "2024-01-01"); + expect(result).toBe(true); + }); + + it("returns false when account_id is missing", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm("", validType, 50, "2024-01-01"); + expect(result).toBe(false); + }); + + it("returns false for invalid transaction type", () => { + const result = validateTransactionForm( + "123", + "INVALID_TYPE", + 50, + "2024-01-01", + ); + expect(result).toBe(false); + }); + + it("returns false for non-positive amount", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm("123", validType, 0, "2024-01-01"); + expect(result).toBe(false); + }); + + it("returns false when date is missing", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm("123", validType, 50, ""); + expect(result).toBe(false); + }); +}); + +describe("validateIncomeForm", () => { + it("returns true for valid name and positive income", () => { + expect(validateIncomeForm("Job", 1000)).toBe(true); + }); + + it("returns false for missing name", () => { + expect(validateIncomeForm("", 1000)).toBe(false); + }); + + it("returns false for non-positive income", () => { + expect(validateIncomeForm("Job", 0)).toBe(false); + }); +}); diff --git a/app/client/test/validateRow.test.ts b/app/client/test/validateRow.test.ts index 19d6621..b6e5a34 100644 --- a/app/client/test/validateRow.test.ts +++ b/app/client/test/validateRow.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { validateRow } from "../src/utils/ValidateRow"; import type { NormalizedRow } from "../src/utils/NormalizeRow"; - +//tests for validateRow function describe("validateRow", () => { it("returns no errors for a fully valid row", () => { const row: NormalizedRow = { From a918b95f242fb395ace2db6c76a5887927dd644f Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 6 Mar 2026 08:52:02 -0600 Subject: [PATCH 164/204] Fixed accounts get request --- app/client/src/components/AccountForm.tsx | 8 ++++---- app/client/src/components/AccountList.tsx | 1 - app/client/src/components/AccountListCard.tsx | 9 +-------- app/server/services/ts/common/types.ts | 2 +- app/server/services/ts/user/src/routes/account.ts | 13 +++++++++---- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index 43c0ed5..a401e80 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -13,7 +13,6 @@ import type { AuthSession } from "@/pages/authTypes.ts"; interface popupProp { toggle: () => void; session: AuthSession; - setAccount?: (account: Account) => void; addAccount?: (account: Account) => void; edit: boolean; selectedAccount?: Account; @@ -24,7 +23,6 @@ type typeofAccount = keyof typeof accountCategory; export default function PopupForm({ toggle, session, - setAccount, addAccount, edit, selectedAccount, @@ -79,9 +77,11 @@ export default function PopupForm({ //Put request to update the account putUserAccount(session, newAccount).then((response) => { //Determine if sucessfully updated the account - if (response && response.lastUpdated && setAccount) { + if (response && response.lastUpdated) { newAccount.last_updated = response.lastUpdated; - setAccount(newAccount); + + //Update the values of the selected account + selectedAccount = { ...newAccount }; } }); } catch { diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx index 4ea9135..10a5fda 100644 --- a/app/client/src/components/AccountList.tsx +++ b/app/client/src/components/AccountList.tsx @@ -54,7 +54,6 @@ export default function AccountList({ session }: listProp) { ))} diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx index f31cc8d..067fc98 100644 --- a/app/client/src/components/AccountListCard.tsx +++ b/app/client/src/components/AccountListCard.tsx @@ -7,16 +7,10 @@ import type { AuthSession } from "@/pages/authTypes.ts"; interface cardProp { account: Account; session: AuthSession; - setAccount: (editAccount: Account) => void; removeAccount: (removeAccount: Account) => void; } -export default function Card({ - account, - session, - setAccount, - removeAccount, -}: cardProp) { +export default function Card({ account, session, removeAccount }: cardProp) { //Stores the value that toggles thhe account popup form const [seen, setSeen] = useState(false); @@ -61,7 +55,6 @@ export default function Card({ diff --git a/app/server/services/ts/common/types.ts b/app/server/services/ts/common/types.ts index 7989578..c27a677 100644 --- a/app/server/services/ts/common/types.ts +++ b/app/server/services/ts/common/types.ts @@ -16,7 +16,7 @@ export interface financialAccount extends RowDataPacket { id: number; name: string; type: string; - balence: number; + balance: number; value: number; last_updated: Date; subtype: string; diff --git a/app/server/services/ts/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts index 17932c1..971abb8 100644 --- a/app/server/services/ts/user/src/routes/account.ts +++ b/app/server/services/ts/user/src/routes/account.ts @@ -7,6 +7,7 @@ import type { ResultSetHeader } from "mysql2"; import { getConnectionPool } from "@/sqlUtil"; import type { financialAccount } from "@/types.js"; import jwt from "jsonwebtoken"; + //import { authenticateJWT } from "../handleJWT.js"; //import { error } from "node:console"; @@ -67,11 +68,15 @@ accountsRouter.get("/", async (req: Request, res: Response) => { if (userId) { try { - const [rows] = await db.query( - `SELECT * FROM financialAccount WHERE id = ?`, + const [rows] = await db.query( + `SELECT * FROM financialAccount JOIN profile_financialAccount pfa + ON financialAccount.id = pfa.financialAccount_id + WHERE pfa.profile_id = ?`, [userId], ); + console.log(rows); + return res.status(200).json(rows); } catch (err) { console.error("Failed to retrieve user's account", err); @@ -116,7 +121,7 @@ accountsRouter.put("/", async (req: Request, res: Response) => { await db.query( `UPDATE financialAccount - SET name = ?, type = ?, balence = ?, value = ?, last_updated = ?, subtype = ? + SET name = ?, type = ?, balance = ?, value = ?, last_updated = ?, subtype = ? WHERE id= ?`, [name, type, balance, value, last_updated, subtype ?? null, id], ); @@ -238,7 +243,7 @@ function isAccount(reqBody: unknown): reqBody is financialAccount { //Checks if the fields match to an financial account return ( typeof check.id === "number" && - typeof check.balence === "number" && + typeof check.balance === "number" && typeof check.type === "string" && typeof check.name === "string" && typeof check.value === "number" && From e8509cba161a3ce6a364b130b1b4155bdf16a68b Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 09:13:26 -0600 Subject: [PATCH 165/204] refactor of analytics service for testing --- .../services/python/analytics/src/__init__.py | 0 .../src/{budget.py => budget_old.py} | 0 .../python/analytics/src/dependencies.py | 37 ++ .../python/analytics/src/logic/__init__.py | 0 .../python/analytics/src/logic/budget.py | 41 ++ .../python/analytics/src/logic/budgetCalc.py | 105 ++++++ .../analytics/src/logic/budgetPerfCalc.py | 94 +++++ .../python/analytics/src/logic/incomeflow.py | 61 +++ .../python/analytics/src/logic/savings.py | 52 +++ .../services/python/analytics/src/main.py | 332 +++-------------- .../services/python/analytics/src/main_old.py | 352 ++++++++++++++++++ .../python/analytics/src/models/schemas.py | 54 +++ .../python/analytics/src/queries/__init__.py | 0 .../python/analytics/src/queries/budget.py | 27 ++ .../analytics/src/queries/incomeflow.py | 15 + .../python/analytics/src/queries/savings.py | 24 ++ .../python/analytics/src/utils/__init__.py | 0 .../python/analytics/src/utils/dates.py | 22 ++ .../src/utils/trans_cat_classifier.py | 40 ++ .../python/analytics/tests/conftest.py | 0 .../python/analytics/tests/test_budget.py | 0 .../python/analytics/tests/test_incomeflow.py | 0 .../python/analytics/tests/test_savings.py | 0 23 files changed, 976 insertions(+), 280 deletions(-) create mode 100644 app/server/services/python/analytics/src/__init__.py rename app/server/services/python/analytics/src/{budget.py => budget_old.py} (100%) create mode 100644 app/server/services/python/analytics/src/dependencies.py create mode 100644 app/server/services/python/analytics/src/logic/__init__.py create mode 100644 app/server/services/python/analytics/src/logic/budget.py create mode 100644 app/server/services/python/analytics/src/logic/budgetCalc.py create mode 100644 app/server/services/python/analytics/src/logic/budgetPerfCalc.py create mode 100644 app/server/services/python/analytics/src/logic/incomeflow.py create mode 100644 app/server/services/python/analytics/src/logic/savings.py create mode 100644 app/server/services/python/analytics/src/main_old.py create mode 100644 app/server/services/python/analytics/src/models/schemas.py create mode 100644 app/server/services/python/analytics/src/queries/__init__.py create mode 100644 app/server/services/python/analytics/src/queries/budget.py create mode 100644 app/server/services/python/analytics/src/queries/incomeflow.py create mode 100644 app/server/services/python/analytics/src/queries/savings.py create mode 100644 app/server/services/python/analytics/src/utils/__init__.py create mode 100644 app/server/services/python/analytics/src/utils/dates.py create mode 100644 app/server/services/python/analytics/src/utils/trans_cat_classifier.py create mode 100644 app/server/services/python/analytics/tests/conftest.py create mode 100644 app/server/services/python/analytics/tests/test_budget.py create mode 100644 app/server/services/python/analytics/tests/test_incomeflow.py create mode 100644 app/server/services/python/analytics/tests/test_savings.py diff --git a/app/server/services/python/analytics/src/__init__.py b/app/server/services/python/analytics/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/src/budget.py b/app/server/services/python/analytics/src/budget_old.py similarity index 100% rename from app/server/services/python/analytics/src/budget.py rename to app/server/services/python/analytics/src/budget_old.py diff --git a/app/server/services/python/analytics/src/dependencies.py b/app/server/services/python/analytics/src/dependencies.py new file mode 100644 index 0000000..33443be --- /dev/null +++ b/app/server/services/python/analytics/src/dependencies.py @@ -0,0 +1,37 @@ +from typing import Optional +from fastapi import HTTPException, Header +import mysql.connector +import os +import jwt +from dotenv import load_dotenv + +load_dotenv() +SECRET_KEY = os.getenv('JWT_SECRET') +ALGORITHM = 'HS256' + +def get_db_connection(): + return mysql.connector.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("DB_NAME") + ) + +async def get_current_user(authorization: Optional[str] = Header(None)): + if not authorization: + raise HTTPException(status_code=401, detail="Authorization header missing") + + try: + scheme, token = authorization.split() + if scheme.lower() != 'bearer': + raise HTTPException(status_code=401, detail="Invalid authentication scheme") + + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id = payload.get('sub') or payload.get('user_id') or payload.get('id') + + if not user_id: + raise HTTPException(status_code=401, detail="User ID not found in token") + + return int(user_id) + except ValueError: + raise HTTPException(status_code=401, detail="Invalid authorization header format") \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/__init__.py b/app/server/services/python/analytics/src/logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/src/logic/budget.py b/app/server/services/python/analytics/src/logic/budget.py new file mode 100644 index 0000000..d34c237 --- /dev/null +++ b/app/server/services/python/analytics/src/logic/budget.py @@ -0,0 +1,41 @@ +from src.logic.budgetCalc import BudgetCalculator +from src.logic.budgetPerfCalc import BudgetPerformanceCalculator +from src.models.schemas import BudgetResponse +from src.queries.budget import get_user_transactions_with_connection +from src.utils.trans_cat_classifier import CategoryClassifier +import pandas as pd + +_default_classifier = CategoryClassifier() +_default_calculator = BudgetCalculator(_default_classifier) +_default_performance = BudgetPerformanceCalculator() + + +#Take all transactions over the last 12 months and sum up the amount for each category found +# Find the top 10 most impactful categories and calculate a budget for each +#At the moment, this uses a 50/30/20 rule, where categories are classified as needs, wants, savings and then a proportional budget is calculated +# Monthly budget is generated based on the average monthly income, but then it can be divided or multiplied to match the specified period (week is divided by 4.33, year multiplied by 12) +def generate_budget(period: str, user_id: int) -> BudgetResponse: + end_date = pd.Timestamp.today() + start_date = end_date - pd.Timedelta(days=365) + + transactions = get_user_transactions_with_connection( + user_id, + start_date.strftime('%Y-%m-%d'), + end_date.strftime('%Y-%m-%d') + ) + + income = [t for t in transactions if t['amount'] > 0] + avg_monthly_income = sum(t['amount'] for t in income) / 12 + + return _default_calculator.generate_budget(transactions, period, avg_monthly_income) + + +#Calculate the performance against a proposed user budget +#returns a dictionary with the performance for each category (budget vs actual) +async def generate_budget_performance( + user_id: int, + budget_response: BudgetResponse, + period: str, + reference_date=None +) -> dict: + return await _default_performance.generate_performance(user_id, budget_response, period, reference_date) \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/budgetCalc.py b/app/server/services/python/analytics/src/logic/budgetCalc.py new file mode 100644 index 0000000..a9e5636 --- /dev/null +++ b/app/server/services/python/analytics/src/logic/budgetCalc.py @@ -0,0 +1,105 @@ +from typing import List, Dict, Tuple +import pandas as pd +from src.models.schemas import BudgetCategory, BudgetResponse +from src.utils.trans_cat_classifier import CategoryClassifier + +class BudgetCalculator: + def __init__(self, classifier: CategoryClassifier): + self.classifier = classifier + self.WEEKS_PER_MONTH = 4.33 + + def separate_income_expenses(self, transactions: List[Dict]) -> Tuple[List[Dict], List[Dict]]: + income = [t for t in transactions if t['amount'] > 0] + expenses = [t for t in transactions if t['amount'] < 0] + return income, expenses + + def calculate_monthly_averages(self, expenses: List[Dict]) -> Dict[str, float]: + expense_by_category = {} + for t in expenses: + cat = t['category'] + amount = abs(t['amount']) + expense_by_category[cat] = expense_by_category.get(cat, 0) + amount + + return {cat: total / 12 for cat, total in expense_by_category.items()} + + def get_top_categories(self, monthly_avg: Dict[str, float], n: int = 10) -> List[Tuple[str, float]]: + return sorted(monthly_avg.items(), key=lambda x: x[1], reverse=True)[:n] + + def calculate_budget_pools(self, avg_monthly_income: float) -> Dict[str, float]: + return { + 'needs': avg_monthly_income * 0.5, + 'wants': avg_monthly_income * 0.3, + 'savings': avg_monthly_income * 0.2 + } + + def calculate_category_budget( + self, + category: str, + avg_spent: float, + category_type: str, + monthly_avg_by_category: Dict[str, float], + category_types: Dict[str, str], + pools: Dict[str, float] + ) -> float: + if category_type == 'need': + pool_total = pools['needs'] + pool_actual = sum( + monthly_avg_by_category.get(c, 0) + for c, t in category_types.items() if t == 'need' + ) + else: # want + pool_total = pools['wants'] + pool_actual = sum( + monthly_avg_by_category.get(c, 0) + for c, t in category_types.items() if t == 'want' + ) + + if pool_actual > 0: + return (avg_spent / pool_actual) * pool_total + return avg_spent * 0.9 # 10% reduction + + def scale_budget(self, amount: float, period: str) -> float: + if period == 'w': + return amount / self.WEEKS_PER_MONTH + elif period == 'm': + return amount + else: # 'y' + return amount * 12 + + def generate_budget( + self, + transactions: List[Dict], + period: str, + avg_monthly_income: float + ) -> BudgetResponse: + """Generate budget from transactions.""" + income, expenses = self.separate_income_expenses(transactions) + monthly_avg = self.calculate_monthly_averages(expenses) + pools = self.calculate_budget_pools(avg_monthly_income) + + category_types = self.classifier.classify_many(set(monthly_avg.keys())) + top_categories = self.get_top_categories(monthly_avg) + + budget_categories = [] + for category, avg_spent in top_categories: + cat_type = category_types.get(category, 'want') + monthly_recommended = self.calculate_category_budget( + category, avg_spent, cat_type, + monthly_avg, category_types, pools + ) + + scaled_recommended = self.scale_budget(monthly_recommended, period) + + budget_categories.append(BudgetCategory( + category=category, + type=cat_type, + avg_monthly_spent=round(avg_spent, 2), + monthly_budget=round(monthly_recommended, 2), + recommended_budget=round(scaled_recommended, 2), + is_essential=cat_type == 'need' + )) + + return BudgetResponse( + budget=budget_categories, + generated_date=pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S') + ) \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/budgetPerfCalc.py b/app/server/services/python/analytics/src/logic/budgetPerfCalc.py new file mode 100644 index 0000000..f58a2b2 --- /dev/null +++ b/app/server/services/python/analytics/src/logic/budgetPerfCalc.py @@ -0,0 +1,94 @@ +from typing import List, Dict, Optional +import pandas as pd +from src.models.schemas import BudgetCategory, BudgetResponse +from src.queries.budget import get_user_transactions_with_connection + +class BudgetPerformanceCalculator: + def __init__(self): + self.SECONDS_PER_DAY = 24 * 60 * 60 - 1 + + def get_period_boundaries(self, period: str, reference_date: Optional[pd.Timestamp] = None): + if reference_date is None: + reference_date = pd.Timestamp.now() + + if period == 'w': + days_to_sunday = (reference_date.dayofweek + 1) % 7 + period_start = (reference_date - pd.Timedelta(days=days_to_sunday)).normalize() + period_end = period_start + pd.Timedelta(days=6, hours=23, minutes=59, seconds=59) + elif period == 'm': + period_start = pd.Timestamp(reference_date.year, reference_date.month, 1) + next_month = period_start + pd.offsets.MonthBegin(1) + period_end = next_month - pd.Timedelta(seconds=1) + else: # 'y' + period_start = pd.Timestamp(reference_date.year, 1, 1) + period_end = pd.Timestamp(reference_date.year, 12, 31, 23, 59, 59) + + return period_start, period_end + + def calculate_actual_spending(self, transactions: List[Dict]) -> Dict[str, float]: + spending = {} + for t in transactions: + if t['amount'] < 0: + cat = t['category'] + amount = abs(t['amount']) + spending[cat] = spending.get(cat, 0) + amount + return spending + + def calculate_performance( + self, + budget_categories: List[BudgetCategory], + actual_spending: Dict[str, float], + period: str + ) -> dict: + performance_items = [] + + for budget_cat in budget_categories: + category = budget_cat.category + budget_amount = budget_cat.recommended_budget + actual = actual_spending.get(category, 0) + + variance = actual - budget_amount + percent_used = (actual / budget_amount * 100) if budget_amount > 0 else 0 + + if variance > 0: + status = 'overspent' + elif variance < 0: + status = 'under_budget' + else: + status = 'on_track' + + performance_items.append({ + 'category': category, + 'budget': round(budget_amount, 2), + 'actual': round(actual, 2), + 'variance': round(variance, 2), + 'percent_used': round(percent_used, 1), + 'status': status + }) + + # sort by highest actual spending + performance_items.sort(key=lambda x: x['actual'], reverse=True) + + return { + 'categories': [p['category'] for p in performance_items], + 'budgetAmounts': [p['budget'] for p in performance_items], + 'actualAmounts': [p['actual'] for p in performance_items] + } + + async def generate_performance( + self, + user_id: int, + budget_response: BudgetResponse, + period: str, + reference_date: Optional[pd.Timestamp] = None + ) -> dict: + period_start, period_end = self.get_period_boundaries(period, reference_date) + + start_str = period_start.strftime('%Y-%m-%d') + end_str = period_end.strftime('%Y-%m-%d') + + transactions = get_user_transactions_with_connection(user_id, start_str, end_str) + actual_spending = self.calculate_actual_spending(transactions) + + performance = self.calculate_performance(budget_response.budget, actual_spending, period) + return performance \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/incomeflow.py b/app/server/services/python/analytics/src/logic/incomeflow.py new file mode 100644 index 0000000..4a08f86 --- /dev/null +++ b/app/server/services/python/analytics/src/logic/incomeflow.py @@ -0,0 +1,61 @@ +import pandas as pd +from typing import List, Dict + +def build_sankey_data(transactions: List[Dict]) -> Dict: + if not transactions: + return { + 'nodes': [{'name': 'total income'}, {'name': 'no data'}], + 'links': [] + } + + df = pd.DataFrame(transactions) + df_agg = df.groupby('category').agg({'amount': 'sum'}).reset_index() + + total_income = df_agg[df_agg['amount'] > 0]['amount'].sum() + total_expenses = abs(df_agg[df_agg['amount'] < 0]['amount'].sum()) + + node_set = {'total income'} + for _, row in df_agg.iterrows(): + node_set.add(row['category']) + + if total_income > total_expenses: + node_set.add('unspent') + elif total_expenses > total_income: + node_set.add('overspent') + node_set.add('savings') + + nodes_list = list(node_set) + node_to_index = {node: idx for idx, node in enumerate(nodes_list)} + links = [] + + for _, row in df_agg[df_agg['amount'] > 0].iterrows(): + links.append({ + 'source': node_to_index[row['category']], + 'target': node_to_index['total income'], + 'value': int(row['amount']) + }) + + for _, row in df_agg[df_agg['amount'] < 0].iterrows(): + links.append({ + 'source': node_to_index['total income'], + 'target': node_to_index[row['category']], + 'value': int(abs(row['amount'])) + }) + + if total_income > total_expenses: + links.append({ + 'source': node_to_index['total income'], + 'target': node_to_index['unspent'], + 'value': int(total_income - total_expenses) + }) + elif total_expenses > total_income: + links.append({ + 'source': node_to_index['savings'], + 'target': node_to_index['overspent'], + 'value': int(total_expenses - total_income) + }) + + return { + 'nodes': [{'name': node} for node in nodes_list], + 'links': links + } \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/savings.py b/app/server/services/python/analytics/src/logic/savings.py new file mode 100644 index 0000000..b70a5a4 --- /dev/null +++ b/app/server/services/python/analytics/src/logic/savings.py @@ -0,0 +1,52 @@ +import pandas as pd +from typing import List, Dict +# from ..queries import savings as savings_queries + +def calculate_savings_over_time( + accounts: List[Dict], + transactions: List[Dict], + start_date, + end_date, + period: str, + period_name: str +) -> Dict: + df = pd.DataFrame(transactions) + if df.empty: + return {'labels': [], 'datasets': [{'label': f'{period_name} Savings', 'data': []}]} + + df['date'] = pd.to_datetime(df['date']) + df = df[df['date'] >= start_date] + df = df.sort_values('date') + + account_balances = {acc['id']: acc['balance'] for acc in accounts} + current_balances = account_balances.copy() + savings_over_time = [] + + if period in ['w', 'm']: + date_range = pd.date_range(start=start_date, end=end_date, freq='D') + for date in date_range: + day_transactions = df[df['date'].dt.date == date.date()] + for _, tx in day_transactions.iterrows(): + current_balances[tx['financialAccount_id']] += tx['amount'] + savings_over_time.append({ + 'date': date.strftime('%Y-%m-%d'), + 'savings': sum(current_balances.values()) + }) + else: + df['year_month'] = df['date'].dt.to_period('M') + monthly_groups = df.groupby('year_month') + for month in pd.date_range(start=start_date, end=end_date, freq='M'): + month_str = month.strftime('%Y-%m') + month_period = pd.Period(month_str, freq='M') + if month_period in monthly_groups.groups: + for _, tx in monthly_groups.get_group(month_period).iterrows(): + current_balances[tx['financialAccount_id']] += tx['amount'] + savings_over_time.append({'date': month_str, 'savings': sum(current_balances.values())}) + + return { + 'labels': [item['date'] for item in savings_over_time], + 'datasets': [{ + 'label': f'{period_name} Savings', + 'data': [item['savings'] for item in savings_over_time] + }] + } \ No newline at end of file diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py index 603928f..240a046 100644 --- a/app/server/services/python/analytics/src/main.py +++ b/app/server/services/python/analytics/src/main.py @@ -1,94 +1,28 @@ -from typing import Optional -from fastapi import FastAPI, HTTPException, Header, Query, Depends +from fastapi import FastAPI, Depends, Query, HTTPException from fastapi.middleware.cors import CORSMiddleware import pandas as pd import uvicorn import os -import mysql.connector as mysql -import src.budget as budget_module -from dotenv import load_dotenv -import jwt +from src.dependencies import get_db_connection, get_current_user +from src.utils.dates import period_calc +from src.logic.budget import generate_budget, generate_budget_performance +from src.queries import savings as savings_queries, incomeflow as incomeflow_queries +from src.logic import savings as savings_service, incomeflow as incomeflow_service -app = FastAPI() -load_dotenv() -SECRET_KEY = os.getenv('JWT_SECRET') -ALGORITHM = 'HS256' -origins = [ - "http://localhost:8080", - "http://localhost:3000",#api gateway - "http://127.0.0.1:8080", - "http://127.0.0.1:3000", - #expand these once deployed -] +app = FastAPI() +# CORS setup +origins = ["http://localhost:8080", "http://localhost:3000", "http://127.0.0.1:8080", "http://127.0.0.1:3000"] app.add_middleware( CORSMiddleware, - allow_origins=["origins"], + allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -def get_db_connection(): - return mysql.connect( - host=os.getenv("MYSQL_HOST"), - user=os.getenv("MYSQL_USER"), - password=os.getenv("MYSQL_PASSWORD"), - database=os.getenv("DB_NAME") - ) - -#helper method to extract user_id from JWT token -async def get_current_user(authorization: Optional[str] = Header(None)): - if not authorization: - raise HTTPException(status_code=401, detail="Authorization header missing") - - try: - scheme, token = authorization.split() - if scheme.lower() != 'bearer': - raise HTTPException(status_code=401, detail="Invalid authentication scheme") - - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - user_id = payload.get('sub') or payload.get('user_id') or payload.get('id')#pretty sure sub works just fine - - if not user_id: - raise HTTPException(status_code=401, detail="User ID not found in token") - - return int(user_id) - - except ValueError: - raise HTTPException(status_code=401, detail="Invalid authorization header format") - - - -# test endpoint -@app.get('/health') -def test_endpoint(): - return 'ok' - - - -def periodCalc(period: str, end_date: pd.Timestamp): - end_date = pd.Timestamp.today() - if period == 'w': - start_date = end_date - pd.Timedelta(days=7) - freq = 'D' - date_format = '%Y-%m-%d' - period_name = 'Weekly' - elif period == 'm': - start_date = end_date - pd.Timedelta(days=30) - freq = 'D' - date_format = '%Y-%m-%d' - period_name = 'Monthly' - else: # 'y' - start_date = end_date - pd.Timedelta(days=365) - freq = 'M' - date_format = '%Y-%m' - period_name = 'Yearly' - - return start_date, freq, date_format, period_name - @app.get('/charts/savings') @@ -97,120 +31,37 @@ async def get_savings(period: str, user_id: int = Depends(get_current_user)): connection = get_db_connection() if not connection: raise HTTPException(status_code=500, detail="Database connection failed") - - #get all savings accounts for user id cursor = connection.cursor(dictionary=True) - cursor.execute(""" - SELECT fa.id, fa.balance - FROM finus.financialAccount fa - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - JOIN finus.finusAccount u ON uap.account_id = u.id - WHERE u.id = %s AND fa.type = 'savings' - """, (user_id,))#coma must be here for this to remain a tuple ;-; - savings_accounts = cursor.fetchall() - - - if not savings_accounts: - return HTTPException(status_code=404, detail="No savings accounts found")#this should be visible to users - - account_ids = [acc['id'] for acc in savings_accounts] - placeholders = ','.join(['%s'] * len(account_ids)) - - #get all transactions for all savings accounts - query = f""" - SELECT - t.financialAccount_id, - t.amount, - t.date - FROM finus.transaction t - WHERE t.financialAccount_id IN ({placeholders}) - ORDER BY t.date ASC - """ - cursor.execute(query, account_ids) - transactions = cursor.fetchall() - if len(transactions) == 0: - return None#doing this to make the icon show up correctly + + # Get savings accounts + accounts = savings_queries.get_savings_accounts(cursor, user_id) + if not accounts: + cursor.close() + connection.close() + return None + + # Get transactions + account_ids = [acc['id'] for acc in accounts] + transactions = savings_queries.get_savings_transactions(cursor, account_ids) cursor.close() connection.close() - - if not transactions: - raise HTTPException(status_code=404, detail="No transactions found")#this should also be visible to users - df = pd.DataFrame(transactions) - df['date'] = pd.to_datetime(df['date']) + if not transactions: + return None - #get date range based on period - week and month preiods display data in days, year period displays data in months + # Calculate date range end_date = pd.Timestamp.today() - - start_date, freq, date_format, period_name = periodCalc(period, end_date) - - # discard transactiosn that are too far back in the past (depending on period) - df = df[df['date'] >= start_date] - - if df.empty: - raise HTTPException(status_code=404, detail=f"No transactions found for {period_name} period") - - # aggregate total balance for each account - account_balances = {acc['id']: acc['balance'] for acc in savings_accounts} - - df = df.sort_values('date') - - current_balances = account_balances.copy() - savings_over_time = [] + start_date, freq, date_format, period_name = period_calc(period, end_date) - if period in ['w', 'm']: - #aggregation on daily basis - date_range = pd.date_range(start=start_date, end=end_date, freq=freq) - for date in date_range: - - day_transactions = df[df['date'].dt.date == date.date()] - - for _, tx in day_transactions.iterrows(): - current_balances[tx['financialAccount_id']] += tx['amount'] - - # update total savings - total_savings = sum(current_balances.values()) - savings_over_time.append({ - 'date': date.strftime(date_format), - 'savings': total_savings - }) - else: - #aggregation on monthly basis - df['year_month'] = df['date'].dt.to_period('M') - monthly_groups = df.groupby('year_month') - - for month in pd.date_range(start=start_date, end=end_date, freq='M'): - month_str = month.strftime('%Y-%m') - month_period = pd.Period(month_str, freq='M') - - if month_period in monthly_groups.groups: - month_txs = monthly_groups.get_group(month_period) - for _, tx in month_txs.iterrows(): - current_balances[tx['financialAccount_id']] += tx['amount'] - - total_savings = sum(current_balances.values()) - savings_over_time.append({ - 'date': month_str, - 'savings': total_savings - }) + # Process data + result = savings_service.calculate_savings_over_time( + accounts, transactions, start_date, end_date, period, period_name + ) - labels = [item['date'] for item in savings_over_time] - data = [item['savings'] for item in savings_over_time] + return result - - - return { - 'labels': labels, - 'datasets': [{ - 'label': f'{period_name} Savings', - 'data': data - }] - } - except Exception as e: print(f"Error: {e}") raise HTTPException(status_code=500, detail=f"Error: {e}") @@ -218,11 +69,10 @@ async def get_savings(period: str, user_id: int = Depends(get_current_user)): @app.get('/charts/incomeflow') -async def get_income_flow(period: str = Query(default='w', enum=['w', 'm', 'y']), user_id: int = Depends(get_current_user)): - #fetch all positive and negative transactions for the user that fall within today and the start of the period (week, month, year) - #aggregate the transactions by category and sum the values for each category - #return the aggregated data as a list of dictionaries with the category as the key and the sum of the values as the value - +async def get_income_flow( + period: str = Query(default='w', enum=['w', 'm', 'y']), + user_id: int = Depends(get_current_user) +): try: connection = get_db_connection() if not connection: @@ -230,115 +80,38 @@ async def get_income_flow(period: str = Query(default='w', enum=['w', 'm', 'y']) cursor = connection.cursor(dictionary=True) - #getting time ranges and formatting based on period + # Calculate date range end_date = pd.Timestamp.today() - start_date, _, _, _= periodCalc(period, end_date) + start_date, _, _, _ = period_calc(period, end_date) + + # Get transactions + transactions = incomeflow_queries.get_user_transactions(cursor, user_id, start_date, end_date) - # get transactions for the period and group by category - query = """ - SELECT - t.amount, - t.category, - t.date - FROM finus.transaction t - JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - JOIN finus.finusAccount u ON uap.account_id = u.id - WHERE u.id = %s - AND t.date BETWEEN %s AND %s - ORDER BY t.date - """ - cursor.execute(query, (user_id, start_date, end_date)) - transactions = cursor.fetchall() - if len(transactions) == 0: - return None cursor.close() connection.close() if not transactions: - #return empty sankey if no transactions are present for the preiod - return { - 'nodes': [ - {'name': 'total income'}, - {'name': 'no data'} - ], - 'links': [] - } - df = pd.DataFrame(transactions) - df_agg = df.groupby('category').agg({'amount': 'sum'}).reset_index() - - #totals are used later to figure out overflow or overspending - total_income = df_agg[df_agg['amount'] > 0]['amount'].sum() - total_expenses = abs(df_agg[df_agg['amount'] < 0]['amount'].sum()) - - node_set = set() - node_set.add('total income')#total income is the starting node that will allways exist - - for _, row in df_agg.iterrows(): - node_set.add(row['category']) - - #add overflow nodes based on income vs expenses - unspent and overspent - #in the case of overspending, the savings node is also added to represent savings being drained (doesn't matter what kind of savings account was drained, realistically this is the chequing acc: cash) - if total_income > total_expenses: - node_set.add('unspent') - elif total_expenses > total_income: - node_set.add('overspent') - node_set.add('savings') - - nodes_list = list(node_set) - node_to_index = {node: idx for idx, node in enumerate(nodes_list)} - - links = [] - - #link positive transaction categories (income streams) to total income - for _, row in df_agg[df_agg['amount'] > 0].iterrows(): - links.append({ - 'source': node_to_index[row['category']], - 'target': node_to_index['total income'], - 'value': int(row['amount']) - }) - - #link total income to negative transaction categories (expenses) - for _, row in df_agg[df_agg['amount'] < 0].iterrows(): - links.append({ - 'source': node_to_index['total income'], - 'target': node_to_index[row['category']], - 'value': int(abs(row['amount'])) - }) - - #overflow/overspending - if total_income > total_expenses: - links.append({ - 'source': node_to_index['total income'], - 'target': node_to_index['unspent'], - 'value': int(total_income - total_expenses) - }) - elif total_expenses > total_income: - links.append({ - 'source': node_to_index['savings'], - 'target': node_to_index['overspent'], - 'value': int(total_expenses - total_income) - }) + return None - return { - 'nodes': [{'name': node} for node in nodes_list], - 'links': links - } + # Build sankey data + return incomeflow_service.build_sankey_data(transactions) except Exception as e: print(f"Error generating income flow: {str(e)}") raise HTTPException(status_code=500, detail=f"Error generating income flow: {str(e)}") -#this will calcualte a budget, and then pass it into performance calculaiton function to generate all the needed values for the chart. -#there are a lot of additional values that could be included, but this is the bare minimum for now + @app.get('/charts/budget-expenditure') -def get_budget(period: str = Query(default='w', enum=['w', 'm', 'y']), user_id: int = Depends(get_current_user)): +async def get_budget( + period: str = Query(default='w', enum=['w', 'm', 'y']), + user_id: int = Depends(get_current_user) +): try: - budget = budget_module.generate_budget(period, user_id) - performance = budget_module.generate_budget_performance(user_id, budget, period) + print(f"Generating budget for user {user_id} with period {period}") + budget = generate_budget(period, user_id) + performance = await generate_budget_performance(user_id, budget, period) + print(f'generated performance: {performance}') return performance except Exception as e: print(e) @@ -346,7 +119,6 @@ def get_budget(period: str = Query(default='w', enum=['w', 'm', 'y']), user_id: - if __name__ == "__main__": port = int(os.getenv("PORT", 8000)) - uvicorn.run(app, port=port) + uvicorn.run(app, port=port) \ No newline at end of file diff --git a/app/server/services/python/analytics/src/main_old.py b/app/server/services/python/analytics/src/main_old.py new file mode 100644 index 0000000..603928f --- /dev/null +++ b/app/server/services/python/analytics/src/main_old.py @@ -0,0 +1,352 @@ +from typing import Optional +from fastapi import FastAPI, HTTPException, Header, Query, Depends +from fastapi.middleware.cors import CORSMiddleware +import pandas as pd +import uvicorn +import os +import mysql.connector as mysql +import src.budget as budget_module +from dotenv import load_dotenv +import jwt + + +app = FastAPI() +load_dotenv() +SECRET_KEY = os.getenv('JWT_SECRET') +ALGORITHM = 'HS256' + +origins = [ + "http://localhost:8080", + "http://localhost:3000",#api gateway + "http://127.0.0.1:8080", + "http://127.0.0.1:3000", + #expand these once deployed +] + +app.add_middleware( + CORSMiddleware, + allow_origins=["origins"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +def get_db_connection(): + return mysql.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("DB_NAME") + ) + +#helper method to extract user_id from JWT token +async def get_current_user(authorization: Optional[str] = Header(None)): + if not authorization: + raise HTTPException(status_code=401, detail="Authorization header missing") + + try: + scheme, token = authorization.split() + if scheme.lower() != 'bearer': + raise HTTPException(status_code=401, detail="Invalid authentication scheme") + + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id = payload.get('sub') or payload.get('user_id') or payload.get('id')#pretty sure sub works just fine + + if not user_id: + raise HTTPException(status_code=401, detail="User ID not found in token") + + return int(user_id) + + except ValueError: + raise HTTPException(status_code=401, detail="Invalid authorization header format") + + + +# test endpoint +@app.get('/health') +def test_endpoint(): + return 'ok' + + + +def periodCalc(period: str, end_date: pd.Timestamp): + end_date = pd.Timestamp.today() + if period == 'w': + start_date = end_date - pd.Timedelta(days=7) + freq = 'D' + date_format = '%Y-%m-%d' + period_name = 'Weekly' + elif period == 'm': + start_date = end_date - pd.Timedelta(days=30) + freq = 'D' + date_format = '%Y-%m-%d' + period_name = 'Monthly' + else: # 'y' + start_date = end_date - pd.Timedelta(days=365) + freq = 'M' + date_format = '%Y-%m' + period_name = 'Yearly' + + return start_date, freq, date_format, period_name + + + +@app.get('/charts/savings') +async def get_savings(period: str, user_id: int = Depends(get_current_user)): + try: + connection = get_db_connection() + if not connection: + raise HTTPException(status_code=500, detail="Database connection failed") + + + #get all savings accounts for user id + cursor = connection.cursor(dictionary=True) + cursor.execute(""" + SELECT fa.id, fa.balance + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + JOIN finus.finusAccount u ON uap.account_id = u.id + WHERE u.id = %s AND fa.type = 'savings' + """, (user_id,))#coma must be here for this to remain a tuple ;-; + savings_accounts = cursor.fetchall() + + + if not savings_accounts: + return HTTPException(status_code=404, detail="No savings accounts found")#this should be visible to users + + account_ids = [acc['id'] for acc in savings_accounts] + placeholders = ','.join(['%s'] * len(account_ids)) + + #get all transactions for all savings accounts + query = f""" + SELECT + t.financialAccount_id, + t.amount, + t.date + FROM finus.transaction t + WHERE t.financialAccount_id IN ({placeholders}) + ORDER BY t.date ASC + """ + cursor.execute(query, account_ids) + transactions = cursor.fetchall() + if len(transactions) == 0: + return None#doing this to make the icon show up correctly + + cursor.close() + connection.close() + + if not transactions: + raise HTTPException(status_code=404, detail="No transactions found")#this should also be visible to users + + df = pd.DataFrame(transactions) + df['date'] = pd.to_datetime(df['date']) + + #get date range based on period - week and month preiods display data in days, year period displays data in months + end_date = pd.Timestamp.today() + + start_date, freq, date_format, period_name = periodCalc(period, end_date) + + # discard transactiosn that are too far back in the past (depending on period) + df = df[df['date'] >= start_date] + + if df.empty: + raise HTTPException(status_code=404, detail=f"No transactions found for {period_name} period") + + # aggregate total balance for each account + account_balances = {acc['id']: acc['balance'] for acc in savings_accounts} + + df = df.sort_values('date') + + current_balances = account_balances.copy() + savings_over_time = [] + + if period in ['w', 'm']: + #aggregation on daily basis + date_range = pd.date_range(start=start_date, end=end_date, freq=freq) + for date in date_range: + + day_transactions = df[df['date'].dt.date == date.date()] + + for _, tx in day_transactions.iterrows(): + current_balances[tx['financialAccount_id']] += tx['amount'] + + # update total savings + total_savings = sum(current_balances.values()) + savings_over_time.append({ + 'date': date.strftime(date_format), + 'savings': total_savings + }) + else: + #aggregation on monthly basis + df['year_month'] = df['date'].dt.to_period('M') + monthly_groups = df.groupby('year_month') + + for month in pd.date_range(start=start_date, end=end_date, freq='M'): + month_str = month.strftime('%Y-%m') + month_period = pd.Period(month_str, freq='M') + + if month_period in monthly_groups.groups: + month_txs = monthly_groups.get_group(month_period) + for _, tx in month_txs.iterrows(): + current_balances[tx['financialAccount_id']] += tx['amount'] + + total_savings = sum(current_balances.values()) + savings_over_time.append({ + 'date': month_str, + 'savings': total_savings + }) + + labels = [item['date'] for item in savings_over_time] + data = [item['savings'] for item in savings_over_time] + + + + return { + 'labels': labels, + 'datasets': [{ + 'label': f'{period_name} Savings', + 'data': data + }] + } + + except Exception as e: + print(f"Error: {e}") + raise HTTPException(status_code=500, detail=f"Error: {e}") + + + +@app.get('/charts/incomeflow') +async def get_income_flow(period: str = Query(default='w', enum=['w', 'm', 'y']), user_id: int = Depends(get_current_user)): + #fetch all positive and negative transactions for the user that fall within today and the start of the period (week, month, year) + #aggregate the transactions by category and sum the values for each category + #return the aggregated data as a list of dictionaries with the category as the key and the sum of the values as the value + + try: + connection = get_db_connection() + if not connection: + raise HTTPException(status_code=500, detail="Database connection failed") + + cursor = connection.cursor(dictionary=True) + + #getting time ranges and formatting based on period + end_date = pd.Timestamp.today() + start_date, _, _, _= periodCalc(period, end_date) + + # get transactions for the period and group by category + query = """ + SELECT + t.amount, + t.category, + t.date + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + JOIN finus.finusAccount u ON uap.account_id = u.id + WHERE u.id = %s + AND t.date BETWEEN %s AND %s + ORDER BY t.date + """ + cursor.execute(query, (user_id, start_date, end_date)) + transactions = cursor.fetchall() + if len(transactions) == 0: + return None + cursor.close() + connection.close() + + if not transactions: + #return empty sankey if no transactions are present for the preiod + return { + 'nodes': [ + {'name': 'total income'}, + {'name': 'no data'} + ], + 'links': [] + } + df = pd.DataFrame(transactions) + df_agg = df.groupby('category').agg({'amount': 'sum'}).reset_index() + + #totals are used later to figure out overflow or overspending + total_income = df_agg[df_agg['amount'] > 0]['amount'].sum() + total_expenses = abs(df_agg[df_agg['amount'] < 0]['amount'].sum()) + + node_set = set() + node_set.add('total income')#total income is the starting node that will allways exist + + for _, row in df_agg.iterrows(): + node_set.add(row['category']) + + #add overflow nodes based on income vs expenses - unspent and overspent + #in the case of overspending, the savings node is also added to represent savings being drained (doesn't matter what kind of savings account was drained, realistically this is the chequing acc: cash) + if total_income > total_expenses: + node_set.add('unspent') + elif total_expenses > total_income: + node_set.add('overspent') + node_set.add('savings') + + nodes_list = list(node_set) + node_to_index = {node: idx for idx, node in enumerate(nodes_list)} + + links = [] + + #link positive transaction categories (income streams) to total income + for _, row in df_agg[df_agg['amount'] > 0].iterrows(): + links.append({ + 'source': node_to_index[row['category']], + 'target': node_to_index['total income'], + 'value': int(row['amount']) + }) + + #link total income to negative transaction categories (expenses) + for _, row in df_agg[df_agg['amount'] < 0].iterrows(): + links.append({ + 'source': node_to_index['total income'], + 'target': node_to_index[row['category']], + 'value': int(abs(row['amount'])) + }) + + #overflow/overspending + if total_income > total_expenses: + links.append({ + 'source': node_to_index['total income'], + 'target': node_to_index['unspent'], + 'value': int(total_income - total_expenses) + }) + elif total_expenses > total_income: + links.append({ + 'source': node_to_index['savings'], + 'target': node_to_index['overspent'], + 'value': int(total_expenses - total_income) + }) + + return { + 'nodes': [{'name': node} for node in nodes_list], + 'links': links + } + + except Exception as e: + print(f"Error generating income flow: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error generating income flow: {str(e)}") + + +#this will calcualte a budget, and then pass it into performance calculaiton function to generate all the needed values for the chart. +#there are a lot of additional values that could be included, but this is the bare minimum for now +@app.get('/charts/budget-expenditure') +def get_budget(period: str = Query(default='w', enum=['w', 'm', 'y']), user_id: int = Depends(get_current_user)): + try: + budget = budget_module.generate_budget(period, user_id) + performance = budget_module.generate_budget_performance(user_id, budget, period) + return performance + except Exception as e: + print(e) + raise HTTPException(status_code=500, detail=str(e)) + + + + +if __name__ == "__main__": + port = int(os.getenv("PORT", 8000)) + uvicorn.run(app, port=port) diff --git a/app/server/services/python/analytics/src/models/schemas.py b/app/server/services/python/analytics/src/models/schemas.py new file mode 100644 index 0000000..bd8b43c --- /dev/null +++ b/app/server/services/python/analytics/src/models/schemas.py @@ -0,0 +1,54 @@ +from pydantic import BaseModel +from typing import List, Literal, Optional +from datetime import datetime + +class SavingsDataPoint(BaseModel): + date: str + savings: float + +class SavingsChartResponse(BaseModel): + labels: List[str] + datasets: List[dict] + +class SankeyNode(BaseModel): + name: str + +class SankeyLink(BaseModel): + source: int + target: int + value: int + +class SankeyResponse(BaseModel): + nodes: List[SankeyNode] + links: List[SankeyLink] + +class Transaction(BaseModel): + amount: float + category: str + date: datetime + account_name: Optional[str] = None + +class BudgetCategory(BaseModel): + category: str + type: Literal['need', 'want', 'savings'] + avg_monthly_spent: float + monthly_budget: float + recommended_budget: float + is_essential: bool + +class BudgetResponse(BaseModel): + budget: List[BudgetCategory] + generated_date: str + +class BudgetPerformanceItem(BaseModel): + category: str + budget: float + actual: float + variance: float + percent_used: float + status: Literal['overspent', 'under_budget', 'on_track'] + +class BudgetPerformanceResponse(BaseModel): + categories: List[str] + budgetAmounts: List[float] + actualAmounts: List[float] \ No newline at end of file diff --git a/app/server/services/python/analytics/src/queries/__init__.py b/app/server/services/python/analytics/src/queries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/src/queries/budget.py b/app/server/services/python/analytics/src/queries/budget.py new file mode 100644 index 0000000..7d1d9a4 --- /dev/null +++ b/app/server/services/python/analytics/src/queries/budget.py @@ -0,0 +1,27 @@ +from typing import List, Dict, Any +from src.dependencies import get_db_connection + +def get_user_transactions(cursor, user_id: int, start_date: str, end_date: str) -> List[Dict[str, Any]]: + """Fetch user transactions within date range.""" + query = """ + SELECT t.amount, t.category, t.date, fa.name as account_name + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + JOIN finus.finusAccount u ON uap.account_id = u.id + WHERE u.id = %s AND t.date BETWEEN %s AND %s + ORDER BY t.date + """ + cursor.execute(query, (user_id, start_date, end_date)) + return cursor.fetchall() + +def get_user_transactions_with_connection(user_id: int, start_date: str, end_date: str): + connection = get_db_connection() + cursor = connection.cursor(dictionary=True) + try: + return get_user_transactions(cursor, user_id, start_date, end_date) + finally: + cursor.close() + connection.close() \ No newline at end of file diff --git a/app/server/services/python/analytics/src/queries/incomeflow.py b/app/server/services/python/analytics/src/queries/incomeflow.py new file mode 100644 index 0000000..c1391e5 --- /dev/null +++ b/app/server/services/python/analytics/src/queries/incomeflow.py @@ -0,0 +1,15 @@ +from typing import List, Dict + +def get_user_transactions(cursor, user_id: int, start_date, end_date) -> List[Dict]: + cursor.execute(""" + SELECT t.amount, t.category, t.date + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + JOIN finus.finusAccount u ON uap.account_id = u.id + WHERE u.id = %s AND t.date BETWEEN %s AND %s + ORDER BY t.date + """, (user_id, start_date, end_date)) + return cursor.fetchall() \ No newline at end of file diff --git a/app/server/services/python/analytics/src/queries/savings.py b/app/server/services/python/analytics/src/queries/savings.py new file mode 100644 index 0000000..e8b3bfd --- /dev/null +++ b/app/server/services/python/analytics/src/queries/savings.py @@ -0,0 +1,24 @@ +from typing import List, Dict +import mysql.connector + +def get_savings_accounts(cursor, user_id: int) -> List[Dict]: + cursor.execute(""" + SELECT fa.id, fa.balance + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + JOIN finus.finusAccount u ON uap.account_id = u.id + WHERE u.id = %s AND fa.type = 'savings' + """, (user_id,)) + return cursor.fetchall() + +def get_savings_transactions(cursor, account_ids: List[int]) -> List[Dict]: + placeholders = ','.join(['%s'] * len(account_ids)) + cursor.execute(f""" + SELECT financialAccount_id, amount, date + FROM finus.transaction + WHERE financialAccount_id IN ({placeholders}) + ORDER BY date ASC + """, account_ids) + return cursor.fetchall() \ No newline at end of file diff --git a/app/server/services/python/analytics/src/utils/__init__.py b/app/server/services/python/analytics/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/src/utils/dates.py b/app/server/services/python/analytics/src/utils/dates.py new file mode 100644 index 0000000..724be06 --- /dev/null +++ b/app/server/services/python/analytics/src/utils/dates.py @@ -0,0 +1,22 @@ +import pandas as pd +from typing import Tuple + +def period_calc(period: str, end_date: pd.Timestamp) -> Tuple[pd.Timestamp, str, str, str]: + """Calculate date range based on period""" + if period == 'w': + start_date = end_date - pd.Timedelta(days=7) + freq = 'D' + date_format = '%Y-%m-%d' + period_name = 'Weekly' + elif period == 'm': + start_date = end_date - pd.Timedelta(days=30) + freq = 'D' + date_format = '%Y-%m-%d' + period_name = 'Monthly' + else: # 'y' + start_date = end_date - pd.Timedelta(days=365) + freq = 'M' + date_format = '%Y-%m' + period_name = 'Yearly' + + return start_date, freq, date_format, period_name \ No newline at end of file diff --git a/app/server/services/python/analytics/src/utils/trans_cat_classifier.py b/app/server/services/python/analytics/src/utils/trans_cat_classifier.py new file mode 100644 index 0000000..0a9cde0 --- /dev/null +++ b/app/server/services/python/analytics/src/utils/trans_cat_classifier.py @@ -0,0 +1,40 @@ +from typing import Set, Dict + +# this handles classification of transaction categories +# expand on these lists in the future - can also make them dynamic with ML +class CategoryClassifier: + def __init__(self): + self.needs_categories: Set[str] = { + 'rent', 'mortgage', 'utilities', 'groceries', 'healthcare', + 'insurance', 'transportation', 'gas', 'electricity', 'water', + 'internet', 'phone', 'minimum_debt_payment' + } + + self.wants_categories: Set[str] = { + 'dining', 'restaurant', 'entertainment', 'shopping', 'clothing', + 'travel', 'vacation', 'hobbies', 'streaming', 'subscriptions', + 'coffee', 'bars', 'alcohol' + } + + self.savings_categories: Set[str] = { + 'savings', 'investment', 'retirement', 'emergency_fund' + } + + def classify(self, category: str) -> str: + """Classify a single category.""" + cat_lower = category.lower() + if cat_lower in self.needs_categories: + return 'need' + elif cat_lower in self.wants_categories: + return 'want' + elif cat_lower in self.savings_categories: + return 'savings' + return 'want' # default + + def classify_many(self, categories: Set[str]) -> Dict[str, str]: + """Classify multiple categories.""" + return {cat: self.classify(cat) for cat in categories} + + def get_categories_by_type(self, category_types: Dict[str, str], target_type: str) -> Set[str]: + """Get all categories of a specific type.""" + return {cat for cat, typ in category_types.items() if typ == target_type} \ No newline at end of file diff --git a/app/server/services/python/analytics/tests/conftest.py b/app/server/services/python/analytics/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/tests/test_budget.py b/app/server/services/python/analytics/tests/test_budget.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/tests/test_incomeflow.py b/app/server/services/python/analytics/tests/test_incomeflow.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/tests/test_savings.py b/app/server/services/python/analytics/tests/test_savings.py new file mode 100644 index 0000000..e69de29 From d048e20e82d85701baaba973df24a726ab7351bf Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 6 Mar 2026 09:29:45 -0600 Subject: [PATCH 166/204] Fix the visual of editing an account in account list --- app/client/src/components/AccountForm.tsx | 9 +++++---- app/client/src/components/AccountList.tsx | 9 +++++++++ app/client/src/components/AccountListCard.tsx | 11 +++++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index a401e80..cea0d43 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -13,6 +13,7 @@ import type { AuthSession } from "@/pages/authTypes.ts"; interface popupProp { toggle: () => void; session: AuthSession; + setAccount?: (account: Account) => void; addAccount?: (account: Account) => void; edit: boolean; selectedAccount?: Account; @@ -23,6 +24,7 @@ type typeofAccount = keyof typeof accountCategory; export default function PopupForm({ toggle, session, + setAccount, addAccount, edit, selectedAccount, @@ -77,11 +79,10 @@ export default function PopupForm({ //Put request to update the account putUserAccount(session, newAccount).then((response) => { //Determine if sucessfully updated the account - if (response && response.lastUpdated) { + if (response && response.lastUpdated && setAccount) { newAccount.last_updated = response.lastUpdated; - - //Update the values of the selected account - selectedAccount = { ...newAccount }; + newAccount.balance = Number(newAccount.balance.toFixed(2)); + setAccount(newAccount); } }); } catch { diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx index 10a5fda..4e87244 100644 --- a/app/client/src/components/AccountList.tsx +++ b/app/client/src/components/AccountList.tsx @@ -42,6 +42,14 @@ export default function AccountList({ session }: listProp) { ); }; + const setAccount = (editAccount: Account) => { + setUserAccounts( + userAccounts.map((account) => + account.id === editAccount.id ? { ...editAccount } : account, + ), + ); + }; + const toggle = () => { setSeen(!seen); }; @@ -55,6 +63,7 @@ export default function AccountList({ session }: listProp) { session={session} account={account} removeAccount={removeAccount} + setAccount={setAccount} /> ))}
diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx index 067fc98..e948138 100644 --- a/app/client/src/components/AccountListCard.tsx +++ b/app/client/src/components/AccountListCard.tsx @@ -7,10 +7,16 @@ import type { AuthSession } from "@/pages/authTypes.ts"; interface cardProp { account: Account; session: AuthSession; + setAccount: (editAccount: Account) => void; removeAccount: (removeAccount: Account) => void; } -export default function Card({ account, session, removeAccount }: cardProp) { +export default function Card({ + account, + session, + setAccount, + removeAccount, +}: cardProp) { //Stores the value that toggles thhe account popup form const [seen, setSeen] = useState(false); @@ -43,7 +49,7 @@ export default function Card({ account, session, removeAccount }: cardProp) {

{account.type + " " + (account.subtype ? " " + account.type : "")}

-

{account.balance}

+

{Number(account.balance).toFixed(2)}

@@ -55,6 +61,7 @@ export default function Card({ account, session, removeAccount }: cardProp) { From 3f20cc0213ff86eaa9d71981daf6829096d70314 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 6 Mar 2026 09:38:41 -0600 Subject: [PATCH 167/204] Fixed where type was being set when editing an account --- app/client/src/components/AccountForm.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index cea0d43..6102a53 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -141,6 +141,7 @@ export default function PopupForm({ interest: 0, }; } else { + //Default value return { name: "", subType: "", @@ -153,12 +154,20 @@ export default function PopupForm({ if (edit && selectedAccount) { return selectedAccount.balance.toString(); } else { + //Default value return ""; } }); const [accountType, setAccountType] = useState( - undefined, + () => { + if (edit && selectedAccount) { + return selectedAccount.type as typeofAccount; + } else { + //Default value + return undefined; + } + }, ); const accountCat: typeofAccount[] = Object.keys( From 2f0fbc5c5d6365b200245dd2f27264b3f888dbce Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 6 Mar 2026 09:55:38 -0600 Subject: [PATCH 168/204] Transaction form set it's fields to the edited transaction if we're editing it. --- app/client/src/components/TransationForm.tsx | 53 ++++++++++++-------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index 2e1ed7c..1f0c146 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -145,11 +145,40 @@ export default function PopupForm({ }, [session]); //Holds state of user input - const [selectedAccount, setSelectedAccount] = useState([]); - const [selectedType, setSelectedType] = useState(""); - const [amount, setAmount] = useState(""); + const [selectedAccount, setSelectedAccount] = useState(() => { + if (edit && selectedTransaction) { + return [selectedTransaction.id.toString()]; + } else { + return []; + } + }); + + const [selectedType, setSelectedType] = useState(() => { + if (edit && selectedTransaction) { + return selectedTransaction.category; + } else { + return ""; + } + }); + + const [amount, setAmount] = useState(() => { + if (edit && selectedTransaction) { + return selectedTransaction.amount.toFixed(2); + } else { + return ""; + } + }); //const [file, setFile] = useState(undefined) - const [selectedDate, setSelectedDate] = useState(""); + + const [selectedDate, setSelectedDate] = useState(() => { + if (edit && selectedTransaction) { + return selectedTransaction.date.toISOString(); + } else { + //Default + return ""; + } + }); + const [other, setOther] = useState(""); //Holds the types of transfers @@ -157,22 +186,6 @@ export default function PopupForm({ transactionCategory, ) as typeOfTransaction[]; - if (edit && selectedTransaction) { - setSelectedType(selectedTransaction.category); - setAmount(selectedTransaction.amount.toString()); - - //Depending the transaction, the user can be the recipient or sender - if (selectedTransaction.category === transactionCategory.INCOME) { - setSelectedAccount([ - selectedTransaction.id.toString(), - selectedTransaction.to, - ]); - setOther(selectedTransaction.from); - } else { - setOther(selectedTransaction.to); - } - } - return ( <>
From 744a68f89d0202ca48d4e3a8be706c451fb99100 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 10:02:28 -0600 Subject: [PATCH 169/204] implemented routing for csv and updated delete in the front end for transactions --- app/client/src/api/Transaction.ts | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index cba7a0b..aab553d 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,6 +1,6 @@ import type { AuthSession } from "@/pages/authTypes"; import type { updateResponse } from "../types/responseTypes"; -import { type Transaction } from "../types/Transaction"; +import type { Transaction } from "../types/Transaction"; const requestUrl = "http://localhost:3000/api/transactions"; @@ -66,7 +66,7 @@ export async function putTranscations( trans: Transaction, ): Promise { try { - const response = await fetch(requestUrl, { + const response = await fetch(`${requestUrl}/${trans.id}`, { method: "PUT", headers: { "content-type": "application/json", @@ -86,35 +86,27 @@ export async function putTranscations( } } +//DELETE /api/transactions/:id + export async function deleteTransaction( session: AuthSession, selectedTransaction: Transaction, -) { +): Promise { try { - const content = JSON.stringify({ - id: selectedTransaction.id, - financialAccount_id: selectedTransaction.financialAccount_id, - }); - //Create delete request to delete the account - const response = await fetch(requestUrl, { + const response = await fetch(`${requestUrl}/${selectedTransaction.id}`, { method: "DELETE", headers: { - "content-type": "application/json", Authorization: `Bearer ${session.token}`, }, - body: content, }); - if (response.ok) { - alert("Transaction has been deleted"); - } else { - alert("Failed to delete transaction"); - console.error(response.status); + if (!response.ok) { + console.error("Failed to delete transaction", response.status); } return response.ok; } catch (error) { - console.log(error); + console.error(error); throw error; } } @@ -126,7 +118,7 @@ export async function uploadCsvTransactions( transactions: Transaction[], ) { try { - const response = await fetch(requestUrl, { + const response = await fetch(`${requestUrl}/csvTransaction`, { method: "POST", headers: { "content-type": "application/json", From c8263c46da647d347353ebde5535e367a52b6f75 Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 11:11:13 -0600 Subject: [PATCH 170/204] unit tests for analytics service - visualization dashboard --- app/server/docker-compose.test.yml | 5 + .../services/python/analytics/Dockerfile.test | 12 + .../python/analytics/requirements-test.txt | 10 + .../python/analytics/requirements.txt | 2 +- .../python/analytics/src/budget_old.py | 240 ------------ .../python/analytics/src/dependencies.py | 10 +- .../python/analytics/src/logic/budgetCalc.py | 2 +- .../analytics/src/logic/budgetPerfCalc.py | 4 +- .../python/analytics/src/logic/savings.py | 2 +- .../services/python/analytics/src/main_old.py | 352 ------------------ .../{tests/conftest.py => test/__init__.py} | 0 .../python/analytics/test/conftest.py | 140 +++++++ .../python/analytics/test/test_budgetCalc.py | 110 ++++++ .../analytics/test/test_budgetPerfCalc.py | 155 ++++++++ .../analytics/test/test_budget_integration.py | 39 ++ .../analytics/test/test_cat_classifier.py | 62 +++ .../analytics/test/test_dependencies.py | 68 ++++ .../python/analytics/test/test_incomeflow.py | 136 +++++++ .../python/analytics/test/test_periodCalc.py | 149 ++++++++ .../python/analytics/test/test_savings.py | 104 ++++++ .../python/analytics/tests/test_budget.py | 0 .../python/analytics/tests/test_incomeflow.py | 0 .../python/analytics/tests/test_savings.py | 0 23 files changed, 1004 insertions(+), 598 deletions(-) create mode 100644 app/server/services/python/analytics/Dockerfile.test create mode 100644 app/server/services/python/analytics/requirements-test.txt delete mode 100644 app/server/services/python/analytics/src/budget_old.py delete mode 100644 app/server/services/python/analytics/src/main_old.py rename app/server/services/python/analytics/{tests/conftest.py => test/__init__.py} (100%) create mode 100644 app/server/services/python/analytics/test/conftest.py create mode 100644 app/server/services/python/analytics/test/test_budgetCalc.py create mode 100644 app/server/services/python/analytics/test/test_budgetPerfCalc.py create mode 100644 app/server/services/python/analytics/test/test_budget_integration.py create mode 100644 app/server/services/python/analytics/test/test_cat_classifier.py create mode 100644 app/server/services/python/analytics/test/test_dependencies.py create mode 100644 app/server/services/python/analytics/test/test_incomeflow.py create mode 100644 app/server/services/python/analytics/test/test_periodCalc.py create mode 100644 app/server/services/python/analytics/test/test_savings.py delete mode 100644 app/server/services/python/analytics/tests/test_budget.py delete mode 100644 app/server/services/python/analytics/tests/test_incomeflow.py delete mode 100644 app/server/services/python/analytics/tests/test_savings.py diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index dccf20a..5f310c4 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -13,3 +13,8 @@ services: build: context: services/ts dockerfile: user/Dockerfile.test + + analytics-test: + build: + context: services/python + dockerfile: analytics/Dockerfile.test diff --git a/app/server/services/python/analytics/Dockerfile.test b/app/server/services/python/analytics/Dockerfile.test new file mode 100644 index 0000000..25b3692 --- /dev/null +++ b/app/server/services/python/analytics/Dockerfile.test @@ -0,0 +1,12 @@ +FROM measureonecodetwice/finus-analytics:local + +COPY analytics/test ./test +COPY analytics/requirements-test.txt . + +RUN pip install -r requirements-test.txt + +WORKDIR /app + +ENV PYTHONPATH=/app + +CMD ["pytest", "test/", "-v", "--cov=src", "--cov-report=term-missing", "--cov-report=xml:coverage.xml"] \ No newline at end of file diff --git a/app/server/services/python/analytics/requirements-test.txt b/app/server/services/python/analytics/requirements-test.txt new file mode 100644 index 0000000..2236104 --- /dev/null +++ b/app/server/services/python/analytics/requirements-test.txt @@ -0,0 +1,10 @@ +pydantic==2.5.0 +pandas==2.1.4 +numpy==1.26.3 +mysql-connector-python==8.0.33 +dotenv==0.9.9 +pyjwt==2.11.0 +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.1.0 +pytest-mock>=3.11.0 \ No newline at end of file diff --git a/app/server/services/python/analytics/requirements.txt b/app/server/services/python/analytics/requirements.txt index 0863746..537ad9f 100644 --- a/app/server/services/python/analytics/requirements.txt +++ b/app/server/services/python/analytics/requirements.txt @@ -5,4 +5,4 @@ pandas==2.1.4 numpy==1.26.3 mysql-connector-python==8.0.33 dotenv==0.9.9 -pyjwt==2.11.0 \ No newline at end of file +pyjwt==2.11.0 diff --git a/app/server/services/python/analytics/src/budget_old.py b/app/server/services/python/analytics/src/budget_old.py deleted file mode 100644 index a62be48..0000000 --- a/app/server/services/python/analytics/src/budget_old.py +++ /dev/null @@ -1,240 +0,0 @@ -from fastapi import HTTPException -import pandas as pd -import mysql.connector as mysql -import os - - -def get_db_connection(): - return mysql.connect( - host=os.getenv("MYSQL_HOST"), - user=os.getenv("MYSQL_USER"), - password=os.getenv("MYSQL_PASSWORD"), - database=os.getenv("DB_NAME") - ) - -#Take all transactions over the last 12 months and sum up the amount for each category found -# Find the top 10 most impactful categories and calculate a budget for each -#At the moment, this uses a 50/30/20 rule, where categories are classified as needs, wants, savings and then a proportional budget is calculated -# Monthly budget is generated based on the average monthly income, but then it can be divided or multiplied to match the specified period (week is divided by 4.33, year multiplied by 12) -# -def generate_budget(period: str, user_id: int): - try: - connection = get_db_connection() - if not connection: - raise HTTPException(status_code=500, detail="Database connection failed") - - cursor = connection.cursor(dictionary=True) - #get last 12 months of transactions for pattern analysis - end_date = pd.Timestamp.today() - start_date = end_date - pd.Timedelta(days=365) - - transactions = get_user_transactions(user_id, start_date, end_date) - - #separate income and expenses - income_transactions = [t for t in transactions if t['amount'] > 0] - expense_transactions = [t for t in transactions if t['amount'] < 0] - - # Calculate average monthly income - total_income = sum(t['amount'] for t in income_transactions) - avg_monthly_income = total_income / 12 # Over 12 months - - #this budget is calculated using the standard 50/30/20 rule for now - can change later to ML - monthly_needs_budget = avg_monthly_income * 0.5 # 50% for essentials - monthly_wants_budget = avg_monthly_income * 0.3 # 30% for discretionary - monthly_savings_budget = avg_monthly_income * 0.2 # 20% for savings/debt - - #group expenses by category - expense_by_category = {} - for t in expense_transactions: - cat = t['category'] - amount = abs(t['amount']) - expense_by_category[cat] = expense_by_category.get(cat, 0) + amount - - # Calculate monthly average per category - monthly_avg_by_category = { - cat: total/12 for cat, total in expense_by_category.items() - } - - #classify categories as needs vs wants - expand on this list if needed in the future -- can also use ML - category_types = classify_categories(monthly_avg_by_category.keys()) - #calculate proportional budgets for top 10 categories - total_expenses = sum(monthly_avg_by_category.values()) - top_categories = sorted( - monthly_avg_by_category.items(), - key=lambda x: x[1], - reverse=True - )[:10] - - budget_categories = [] - for category, avg_spent in top_categories: - cat_type = category_types.get(category, 'want')#default to want if no category is found - - #determine which budget pool this comes from - if cat_type == 'need': - pool_total = monthly_needs_budget - pool_actual = sum(monthly_avg_by_category.get(c, 0) - for c, t in category_types.items() if t == 'need') - else: # want - pool_total = monthly_wants_budget - pool_actual = sum(monthly_avg_by_category.get(c, 0) - for c, t in category_types.items() if t == 'want') - - #proportional allocation - if pool_actual > 0: - monthly_recommended = (avg_spent / pool_actual) * pool_total - else: - monthly_recommended = avg_spent * 0.9 #default 10% reduction to prioritize savings - - if period == 'w': - scaled_recommended = monthly_recommended / 4.33 - elif period == 'm': - scaled_recommended = monthly_recommended - else: # yearly - scaled_recommended = monthly_recommended * 12 - - budget_categories.append({ - 'category': category, - 'type': cat_type, - 'avg_monthly_spent': round(avg_spent, 2), - 'monthly_budget': round(monthly_recommended, 2), - 'recommended_budget': round(scaled_recommended, 2), - 'is_essential': cat_type == 'need' - }) - - return { - 'budget': budget_categories, - 'generated_date': pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')#iso format for date time in case we want to cache - } - - except Exception as e: - print(f"Error generating budget: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error generating budget: {str(e)}") - - -#helper method to classify categories into needs, wants and savings - can expand on this in the future or dynamically adjust these lists for each user -def classify_categories(categories): - needs_categories = { - 'rent', 'mortgage', 'utilities', 'groceries', 'healthcare', - 'insurance', 'transportation', 'gas', 'electricity', 'water', - 'internet', 'phone', 'minimum_debt_payment' - } - - wants_categories = { - 'dining', 'restaurant', 'entertainment', 'shopping', 'clothing', - 'travel', 'vacation', 'hobbies', 'streaming', 'subscriptions', - 'coffee', 'bars', 'alcohol' - } - - savings_categories = { - 'savings', 'investment', 'retirement', 'emergency_fund' - } - - result = {} - for cat in categories: - cat_lower = cat.lower() - if cat_lower in needs_categories: - result[cat] = 'need' - elif cat_lower in wants_categories: - result[cat] = 'want' - elif cat_lower in savings_categories: - result[cat] = 'savings' - else: - #default to want to be safe - result[cat] = 'want' - - return result - - -#helper method to get user's transactions -def get_user_transactions(user_id: int, start_date: str, end_date: str): - connection = get_db_connection() - cursor = connection.cursor(dictionary=True) - - query = """ - SELECT t.*, fa.name as account_name - FROM finus.transaction t - JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - JOIN finus.finusAccount u ON uap.account_id = u.id - WHERE u.id = %s - AND t.date BETWEEN %s AND %s - ORDER BY t.date - """ - cursor.execute(query, (user_id, start_date, end_date)) - transactions = cursor.fetchall() - cursor.close() - connection.close() - - return transactions - - - -def generate_budget_performance(user_id, budget, period='monthly', reference_date=None): - - if reference_date is None: - reference_date = pd.Timestamp.now() - - #get boundaries based on calendar - if period == 'w': - #week starts on Sunday - days_to_sunday = (reference_date.dayofweek + 1) % 7 - period_start = (reference_date - pd.Timedelta(days=days_to_sunday)).normalize() - period_end = period_start + pd.Timedelta(days=6, hours=23, minutes=59, seconds=59) - - elif period == 'm': - #first day of current month - period_start = pd.Timestamp(reference_date.year, reference_date.month, 1) - #last day of current month - next_month = period_start + pd.offsets.MonthBegin(1) - period_end = next_month - pd.Timedelta(seconds=1) - - else: # yearly - period_start = pd.Timestamp(reference_date.year, 1, 1) - period_end = pd.Timestamp(reference_date.year, 12, 31, 23, 59, 59) - - start_str = period_start.strftime('%Y-%m-%d') - end_str = period_end.strftime('%Y-%m-%d') - - transactions = get_user_transactions(user_id, start_str, end_str) - - actual_spending = {} - for t in transactions: - if t['amount'] < 0: - cat = t['category'] - amount = abs(t['amount']) - actual_spending[cat] = actual_spending.get(cat, 0) + amount - - #compare with budget - performance = [] - - for budget_cat in budget['budget']: - # Make sure budget_cat is a dictionary, not a string - #print(budget_cat) - category = budget_cat.get('category', 'unknown') - budget_amount = budget_cat.get('recommended_budget', 0) - - # Use the already scaled budget amount - actual = actual_spending.get(category, 0) - - variance = actual - budget_amount - percent_used = (actual / budget_amount * 100) if budget_amount > 0 else 0 - - performance.append({ - 'category': category, - 'budget': round(budget_amount, 2), - 'actual': round(actual, 2), - 'variance': round(variance, 2), - 'percent_used': round(percent_used, 1), - 'status': 'overspent' if variance > 0 else 'under_budget' if variance < 0 else 'on_track' - }) - - - #sort by highest actual spending - performance.sort(key=lambda x: x['actual'], reverse=True) - return { - 'categories': [p['category'] for p in performance], - 'budgetAmounts': [p['budget'] for p in performance], - 'actualAmounts': [p['actual'] for p in performance], - } diff --git a/app/server/services/python/analytics/src/dependencies.py b/app/server/services/python/analytics/src/dependencies.py index 33443be..4bbad18 100644 --- a/app/server/services/python/analytics/src/dependencies.py +++ b/app/server/services/python/analytics/src/dependencies.py @@ -10,6 +10,9 @@ ALGORITHM = 'HS256' def get_db_connection(): + #check env vars + if not all([os.getenv("MYSQL_HOST"), os.getenv("MYSQL_USER"), os.getenv("MYSQL_PASSWORD"), os.getenv("DB_NAME")]): + raise HTTPException(status_code=500, detail="Missing environment variables") return mysql.connector.connect( host=os.getenv("MYSQL_HOST"), user=os.getenv("MYSQL_USER"), @@ -27,11 +30,16 @@ async def get_current_user(authorization: Optional[str] = Header(None)): raise HTTPException(status_code=401, detail="Invalid authentication scheme") payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - user_id = payload.get('sub') or payload.get('user_id') or payload.get('id') + user_id = payload.get('sub') if not user_id: raise HTTPException(status_code=401, detail="User ID not found in token") return int(user_id) + + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="Token expired") + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Invalid token") except ValueError: raise HTTPException(status_code=401, detail="Invalid authorization header format") \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/budgetCalc.py b/app/server/services/python/analytics/src/logic/budgetCalc.py index a9e5636..afe6367 100644 --- a/app/server/services/python/analytics/src/logic/budgetCalc.py +++ b/app/server/services/python/analytics/src/logic/budgetCalc.py @@ -56,7 +56,7 @@ def calculate_category_budget( if pool_actual > 0: return (avg_spent / pool_actual) * pool_total - return avg_spent * 0.9 # 10% reduction + return avg_spent * 0.9 # try to reduce spending by 10% if there is absolutely no income def scale_budget(self, amount: float, period: str) -> float: if period == 'w': diff --git a/app/server/services/python/analytics/src/logic/budgetPerfCalc.py b/app/server/services/python/analytics/src/logic/budgetPerfCalc.py index f58a2b2..215984f 100644 --- a/app/server/services/python/analytics/src/logic/budgetPerfCalc.py +++ b/app/server/services/python/analytics/src/logic/budgetPerfCalc.py @@ -50,9 +50,9 @@ def calculate_performance( variance = actual - budget_amount percent_used = (actual / budget_amount * 100) if budget_amount > 0 else 0 - if variance > 0: + if variance > 100: status = 'overspent' - elif variance < 0: + elif variance < -100: status = 'under_budget' else: status = 'on_track' diff --git a/app/server/services/python/analytics/src/logic/savings.py b/app/server/services/python/analytics/src/logic/savings.py index b70a5a4..f3c4387 100644 --- a/app/server/services/python/analytics/src/logic/savings.py +++ b/app/server/services/python/analytics/src/logic/savings.py @@ -35,7 +35,7 @@ def calculate_savings_over_time( else: df['year_month'] = df['date'].dt.to_period('M') monthly_groups = df.groupby('year_month') - for month in pd.date_range(start=start_date, end=end_date, freq='M'): + for month in pd.date_range(start=start_date, end=end_date, freq='ME'): month_str = month.strftime('%Y-%m') month_period = pd.Period(month_str, freq='M') if month_period in monthly_groups.groups: diff --git a/app/server/services/python/analytics/src/main_old.py b/app/server/services/python/analytics/src/main_old.py deleted file mode 100644 index 603928f..0000000 --- a/app/server/services/python/analytics/src/main_old.py +++ /dev/null @@ -1,352 +0,0 @@ -from typing import Optional -from fastapi import FastAPI, HTTPException, Header, Query, Depends -from fastapi.middleware.cors import CORSMiddleware -import pandas as pd -import uvicorn -import os -import mysql.connector as mysql -import src.budget as budget_module -from dotenv import load_dotenv -import jwt - - -app = FastAPI() -load_dotenv() -SECRET_KEY = os.getenv('JWT_SECRET') -ALGORITHM = 'HS256' - -origins = [ - "http://localhost:8080", - "http://localhost:3000",#api gateway - "http://127.0.0.1:8080", - "http://127.0.0.1:3000", - #expand these once deployed -] - -app.add_middleware( - CORSMiddleware, - allow_origins=["origins"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -def get_db_connection(): - return mysql.connect( - host=os.getenv("MYSQL_HOST"), - user=os.getenv("MYSQL_USER"), - password=os.getenv("MYSQL_PASSWORD"), - database=os.getenv("DB_NAME") - ) - -#helper method to extract user_id from JWT token -async def get_current_user(authorization: Optional[str] = Header(None)): - if not authorization: - raise HTTPException(status_code=401, detail="Authorization header missing") - - try: - scheme, token = authorization.split() - if scheme.lower() != 'bearer': - raise HTTPException(status_code=401, detail="Invalid authentication scheme") - - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - user_id = payload.get('sub') or payload.get('user_id') or payload.get('id')#pretty sure sub works just fine - - if not user_id: - raise HTTPException(status_code=401, detail="User ID not found in token") - - return int(user_id) - - except ValueError: - raise HTTPException(status_code=401, detail="Invalid authorization header format") - - - -# test endpoint -@app.get('/health') -def test_endpoint(): - return 'ok' - - - -def periodCalc(period: str, end_date: pd.Timestamp): - end_date = pd.Timestamp.today() - if period == 'w': - start_date = end_date - pd.Timedelta(days=7) - freq = 'D' - date_format = '%Y-%m-%d' - period_name = 'Weekly' - elif period == 'm': - start_date = end_date - pd.Timedelta(days=30) - freq = 'D' - date_format = '%Y-%m-%d' - period_name = 'Monthly' - else: # 'y' - start_date = end_date - pd.Timedelta(days=365) - freq = 'M' - date_format = '%Y-%m' - period_name = 'Yearly' - - return start_date, freq, date_format, period_name - - - -@app.get('/charts/savings') -async def get_savings(period: str, user_id: int = Depends(get_current_user)): - try: - connection = get_db_connection() - if not connection: - raise HTTPException(status_code=500, detail="Database connection failed") - - - #get all savings accounts for user id - cursor = connection.cursor(dictionary=True) - cursor.execute(""" - SELECT fa.id, fa.balance - FROM finus.financialAccount fa - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - JOIN finus.finusAccount u ON uap.account_id = u.id - WHERE u.id = %s AND fa.type = 'savings' - """, (user_id,))#coma must be here for this to remain a tuple ;-; - savings_accounts = cursor.fetchall() - - - if not savings_accounts: - return HTTPException(status_code=404, detail="No savings accounts found")#this should be visible to users - - account_ids = [acc['id'] for acc in savings_accounts] - placeholders = ','.join(['%s'] * len(account_ids)) - - #get all transactions for all savings accounts - query = f""" - SELECT - t.financialAccount_id, - t.amount, - t.date - FROM finus.transaction t - WHERE t.financialAccount_id IN ({placeholders}) - ORDER BY t.date ASC - """ - cursor.execute(query, account_ids) - transactions = cursor.fetchall() - if len(transactions) == 0: - return None#doing this to make the icon show up correctly - - cursor.close() - connection.close() - - if not transactions: - raise HTTPException(status_code=404, detail="No transactions found")#this should also be visible to users - - df = pd.DataFrame(transactions) - df['date'] = pd.to_datetime(df['date']) - - #get date range based on period - week and month preiods display data in days, year period displays data in months - end_date = pd.Timestamp.today() - - start_date, freq, date_format, period_name = periodCalc(period, end_date) - - # discard transactiosn that are too far back in the past (depending on period) - df = df[df['date'] >= start_date] - - if df.empty: - raise HTTPException(status_code=404, detail=f"No transactions found for {period_name} period") - - # aggregate total balance for each account - account_balances = {acc['id']: acc['balance'] for acc in savings_accounts} - - df = df.sort_values('date') - - current_balances = account_balances.copy() - savings_over_time = [] - - if period in ['w', 'm']: - #aggregation on daily basis - date_range = pd.date_range(start=start_date, end=end_date, freq=freq) - for date in date_range: - - day_transactions = df[df['date'].dt.date == date.date()] - - for _, tx in day_transactions.iterrows(): - current_balances[tx['financialAccount_id']] += tx['amount'] - - # update total savings - total_savings = sum(current_balances.values()) - savings_over_time.append({ - 'date': date.strftime(date_format), - 'savings': total_savings - }) - else: - #aggregation on monthly basis - df['year_month'] = df['date'].dt.to_period('M') - monthly_groups = df.groupby('year_month') - - for month in pd.date_range(start=start_date, end=end_date, freq='M'): - month_str = month.strftime('%Y-%m') - month_period = pd.Period(month_str, freq='M') - - if month_period in monthly_groups.groups: - month_txs = monthly_groups.get_group(month_period) - for _, tx in month_txs.iterrows(): - current_balances[tx['financialAccount_id']] += tx['amount'] - - total_savings = sum(current_balances.values()) - savings_over_time.append({ - 'date': month_str, - 'savings': total_savings - }) - - labels = [item['date'] for item in savings_over_time] - data = [item['savings'] for item in savings_over_time] - - - - return { - 'labels': labels, - 'datasets': [{ - 'label': f'{period_name} Savings', - 'data': data - }] - } - - except Exception as e: - print(f"Error: {e}") - raise HTTPException(status_code=500, detail=f"Error: {e}") - - - -@app.get('/charts/incomeflow') -async def get_income_flow(period: str = Query(default='w', enum=['w', 'm', 'y']), user_id: int = Depends(get_current_user)): - #fetch all positive and negative transactions for the user that fall within today and the start of the period (week, month, year) - #aggregate the transactions by category and sum the values for each category - #return the aggregated data as a list of dictionaries with the category as the key and the sum of the values as the value - - try: - connection = get_db_connection() - if not connection: - raise HTTPException(status_code=500, detail="Database connection failed") - - cursor = connection.cursor(dictionary=True) - - #getting time ranges and formatting based on period - end_date = pd.Timestamp.today() - start_date, _, _, _= periodCalc(period, end_date) - - # get transactions for the period and group by category - query = """ - SELECT - t.amount, - t.category, - t.date - FROM finus.transaction t - JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id - JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id - JOIN finus.profile p ON pfa.profile_id = p.id - JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id - JOIN finus.finusAccount u ON uap.account_id = u.id - WHERE u.id = %s - AND t.date BETWEEN %s AND %s - ORDER BY t.date - """ - cursor.execute(query, (user_id, start_date, end_date)) - transactions = cursor.fetchall() - if len(transactions) == 0: - return None - cursor.close() - connection.close() - - if not transactions: - #return empty sankey if no transactions are present for the preiod - return { - 'nodes': [ - {'name': 'total income'}, - {'name': 'no data'} - ], - 'links': [] - } - df = pd.DataFrame(transactions) - df_agg = df.groupby('category').agg({'amount': 'sum'}).reset_index() - - #totals are used later to figure out overflow or overspending - total_income = df_agg[df_agg['amount'] > 0]['amount'].sum() - total_expenses = abs(df_agg[df_agg['amount'] < 0]['amount'].sum()) - - node_set = set() - node_set.add('total income')#total income is the starting node that will allways exist - - for _, row in df_agg.iterrows(): - node_set.add(row['category']) - - #add overflow nodes based on income vs expenses - unspent and overspent - #in the case of overspending, the savings node is also added to represent savings being drained (doesn't matter what kind of savings account was drained, realistically this is the chequing acc: cash) - if total_income > total_expenses: - node_set.add('unspent') - elif total_expenses > total_income: - node_set.add('overspent') - node_set.add('savings') - - nodes_list = list(node_set) - node_to_index = {node: idx for idx, node in enumerate(nodes_list)} - - links = [] - - #link positive transaction categories (income streams) to total income - for _, row in df_agg[df_agg['amount'] > 0].iterrows(): - links.append({ - 'source': node_to_index[row['category']], - 'target': node_to_index['total income'], - 'value': int(row['amount']) - }) - - #link total income to negative transaction categories (expenses) - for _, row in df_agg[df_agg['amount'] < 0].iterrows(): - links.append({ - 'source': node_to_index['total income'], - 'target': node_to_index[row['category']], - 'value': int(abs(row['amount'])) - }) - - #overflow/overspending - if total_income > total_expenses: - links.append({ - 'source': node_to_index['total income'], - 'target': node_to_index['unspent'], - 'value': int(total_income - total_expenses) - }) - elif total_expenses > total_income: - links.append({ - 'source': node_to_index['savings'], - 'target': node_to_index['overspent'], - 'value': int(total_expenses - total_income) - }) - - return { - 'nodes': [{'name': node} for node in nodes_list], - 'links': links - } - - except Exception as e: - print(f"Error generating income flow: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error generating income flow: {str(e)}") - - -#this will calcualte a budget, and then pass it into performance calculaiton function to generate all the needed values for the chart. -#there are a lot of additional values that could be included, but this is the bare minimum for now -@app.get('/charts/budget-expenditure') -def get_budget(period: str = Query(default='w', enum=['w', 'm', 'y']), user_id: int = Depends(get_current_user)): - try: - budget = budget_module.generate_budget(period, user_id) - performance = budget_module.generate_budget_performance(user_id, budget, period) - return performance - except Exception as e: - print(e) - raise HTTPException(status_code=500, detail=str(e)) - - - - -if __name__ == "__main__": - port = int(os.getenv("PORT", 8000)) - uvicorn.run(app, port=port) diff --git a/app/server/services/python/analytics/tests/conftest.py b/app/server/services/python/analytics/test/__init__.py similarity index 100% rename from app/server/services/python/analytics/tests/conftest.py rename to app/server/services/python/analytics/test/__init__.py diff --git a/app/server/services/python/analytics/test/conftest.py b/app/server/services/python/analytics/test/conftest.py new file mode 100644 index 0000000..d06cbc4 --- /dev/null +++ b/app/server/services/python/analytics/test/conftest.py @@ -0,0 +1,140 @@ +import pytest +from unittest.mock import patch +from datetime import datetime, timedelta +from src.models.schemas import BudgetCategory, BudgetResponse +from src.utils.trans_cat_classifier import CategoryClassifier +import jwt +import os + +@pytest.fixture +def sample_transactions(): + """Provide sample transaction data.""" + base_date = datetime.now() - timedelta(days=30) + return [ + {'amount': 5000, 'category': 'salary', 'date': base_date}, + {'amount': 200, 'category': 'groceries', 'date': base_date + timedelta(days=2)}, + {'amount': -50, 'category': 'dining', 'date': base_date + timedelta(days=3)}, + {'amount': -100, 'category': 'groceries', 'date': base_date + timedelta(days=5)}, + {'amount': -30, 'category': 'coffee', 'date': base_date + timedelta(days=7)}, + {'amount': -200, 'category': 'entertainment', 'date': base_date + timedelta(days=10)}, + {'amount': 5000, 'category': 'salary', 'date': base_date + timedelta(days=30)}, + ] + +@pytest.fixture +def sample_budget_response(): + categories = [ + BudgetCategory( + category='groceries', + type='need', + avg_monthly_spent=300, + monthly_budget=280, + recommended_budget=65, # weekly + is_essential=True + ), + BudgetCategory( + category='dining', + type='want', + avg_monthly_spent=200, + monthly_budget=150, + recommended_budget=35, # weekly + is_essential=False + ), + BudgetCategory( + category='entertainment', + type='want', + avg_monthly_spent=150, + monthly_budget=120, + recommended_budget=28, # weekly + is_essential=False + ), + ] + return BudgetResponse( + budget=categories, + generated_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ) + +@pytest.fixture +def classifier(): + return CategoryClassifier() + +@pytest.fixture(autouse=True) +def mock_db_queries(): + with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock: + yield mock + + +@pytest.fixture +def mock_savings_accounts(): + """Mock savings accounts data.""" + return [ + {'id': 1, 'balance': 5000}, + {'id': 2, 'balance': 3000}, + ] + +@pytest.fixture +def mock_savings_transactions(): + """Mock transactions for savings accounts.""" + base_date = datetime.now() - timedelta(days=30) + return [ + {'financialAccount_id': 1, 'amount': 100, 'date': base_date + timedelta(days=5)}, + {'financialAccount_id': 1, 'amount': 200, 'date': base_date + timedelta(days=10)}, + {'financialAccount_id': 2, 'amount': -50, 'date': base_date + timedelta(days=7)}, + {'financialAccount_id': 2, 'amount': 150, 'date': base_date + timedelta(days=15)}, + {'financialAccount_id': 1, 'amount': -75, 'date': base_date + timedelta(days=20)}, + ] + +@pytest.fixture +def mock_incomeflow_transactions(): + """Mock transactions for income flow (mix of income and expenses).""" + base_date = datetime.now() - timedelta(days=30) + return [ + # Income transactions + {'amount': 5000, 'category': 'salary', 'date': base_date}, + {'amount': 200, 'category': 'freelance', 'date': base_date + timedelta(days=7)}, + {'amount': 100, 'category': 'interest', 'date': base_date + timedelta(days=14)}, + # Expense transactions + {'amount': -1500, 'category': 'rent', 'date': base_date + timedelta(days=1)}, + {'amount': -300, 'category': 'groceries', 'date': base_date + timedelta(days=3)}, + {'amount': -200, 'category': 'dining', 'date': base_date + timedelta(days=5)}, + {'amount': -100, 'category': 'entertainment', 'date': base_date + timedelta(days=8)}, + {'amount': -400, 'category': 'utilities', 'date': base_date + timedelta(days=12)}, + ] + + +@pytest.fixture +def mock_env_vars(): + """Mock environment variables for testing.""" + with patch.dict(os.environ, { + 'JWT_SECRET': 'test_secret_key_12345', + 'MYSQL_HOST': 'localhost', + 'MYSQL_USER': 'test_user', + 'MYSQL_PASSWORD': 'test_password', + 'DB_NAME': 'test_db' + }): + yield + +@pytest.fixture +def valid_token(): + """Generate a valid JWT token for testing.""" + payload = {'sub': '123', 'user_id': '123', 'id': '123'} + return jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') + +@pytest.fixture +def expired_token(): + """Generate an expired JWT token.""" + import time + payload = {'sub': '123', 'exp': int(time.time()) - 3600} + return jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') + +@pytest.fixture +def invalid_token(): + """Generate an invalid JWT token.""" + return "invalid.token.string" + +@pytest.fixture +def mock_db_connection(): + """Mock database connection.""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + return mock_conn \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_budgetCalc.py b/app/server/services/python/analytics/test/test_budgetCalc.py new file mode 100644 index 0000000..fd89755 --- /dev/null +++ b/app/server/services/python/analytics/test/test_budgetCalc.py @@ -0,0 +1,110 @@ +import pytest +from unittest.mock import Mock, patch +from src.logic.budgetCalc import BudgetCalculator +from src.utils.trans_cat_classifier import CategoryClassifier +from src.queries import budget + +class TestBudgetCalculator: + + @pytest.fixture + def calculator(self): + classifier = CategoryClassifier() + return BudgetCalculator(classifier) + + def test_separate_income_expenses(self, calculator, sample_transactions): + """Test separating income and expenses.""" + income, expenses = calculator.separate_income_expenses(sample_transactions) + + assert len(income) == 3 # Three positive transactions + assert len(expenses) == 4 # Four expense transactions + assert all(t['amount'] > 0 for t in income) + assert all(t['amount'] < 0 for t in expenses) + + def test_calculate_monthly_averages(self, calculator): + """Test calculating monthly averages per category.""" + expenses = [ + {'category': 'groceries', 'amount': -300}, + {'category': 'groceries', 'amount': -150}, + {'category': 'dining', 'amount': -200}, + {'category': 'entertainment', 'amount': -100}, + ] + + # With 12 months of data, the function divides by 12 + result = calculator.calculate_monthly_averages(expenses) + + assert result['groceries'] == 450 / 12 # 37.5 + assert result['dining'] == 200 / 12 # 16.67 + assert result['entertainment'] == 100 / 12 # 8.33 + + def test_get_top_categories(self, calculator): + """Test getting top N categories by spending.""" + monthly_avg = { + 'groceries': 500, + 'rent': 1500, + 'dining': 300, + 'entertainment': 200, + 'utilities': 400 + } + + top_3 = calculator.get_top_categories(monthly_avg, 3) + assert len(top_3) == 3 + assert top_3[0][0] == 'rent' # Highest + assert top_3[1][0] == 'groceries' + assert top_3[2][0] == 'utilities' + + def test_calculate_budget_pools(self, calculator): + """Test 50/30/20 budget pool calculation.""" + avg_income = 5000 + pools = calculator.calculate_budget_pools(avg_income) + + assert pools['needs'] == 2500 # 50% + assert pools['wants'] == 1500 # 30% + assert pools['savings'] == 1000 # 20% + + def test_calculate_category_budget_need(self, calculator): + """Test budget calculation for need category.""" + monthly_avg = {'rent': 1200, 'groceries': 400} + category_types = {'rent': 'need', 'groceries': 'need'} + pools = {'needs': 2000, 'wants': 1000, 'savings': 500} + + result = calculator.calculate_category_budget( + 'rent', 1200, 'need', monthly_avg, category_types, pools + ) + + # rent is 1200/1600 of needs pool = 1500 + assert result == 1500 + + def test_calculate_category_budget_want(self, calculator): + """Test budget calculation for want category.""" + monthly_avg = {'dining': 500, 'entertainment': 300} + category_types = {'dining': 'want', 'entertainment': 'want'} + pools = {'needs': 2000, 'wants': 1000, 'savings': 500} + + result = calculator.calculate_category_budget( + 'dining', 500, 'want', monthly_avg, category_types, pools + ) + + # dining is 500/800 of wants pool = 625 + assert result == 625 + + def test_scale_budget(self, calculator): + """Test budget scaling for different periods.""" + monthly_amount = 1000 + + weekly = calculator.scale_budget(monthly_amount, 'w') + monthly = calculator.scale_budget(monthly_amount, 'm') + yearly = calculator.scale_budget(monthly_amount, 'y') + + assert weekly == 1000 / 4.33 + assert monthly == 1000 + assert yearly == 12000 + + @pytest.mark.asyncio + async def test_generate_budget(self, calculator, sample_transactions): + avg_income = 5000 + + result = calculator.generate_budget(sample_transactions, 'w', avg_income) + + assert hasattr(result, 'budget') + assert hasattr(result, 'generated_date') + assert len(result.budget) <= 10 # At most 10 categories \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_budgetPerfCalc.py b/app/server/services/python/analytics/test/test_budgetPerfCalc.py new file mode 100644 index 0000000..e267b59 --- /dev/null +++ b/app/server/services/python/analytics/test/test_budgetPerfCalc.py @@ -0,0 +1,155 @@ +import pytest +import pandas as pd +from datetime import datetime +from unittest.mock import patch, ANY +from src.logic.budgetPerfCalc import BudgetPerformanceCalculator +from src.models.schemas import BudgetResponse, BudgetCategory + +pytestmark = pytest.mark.asyncio + +class TestBudgetPerformanceCalculator: + + @pytest.fixture + def performance_calc(self): + return BudgetPerformanceCalculator() + + @pytest.fixture + def mock_transactions(self): + return [ + {'category': 'groceries', 'amount': -300, 'date': '2024-03-10'}, + {'category': 'groceries', 'amount': -150, 'date': '2024-03-12'}, + {'category': 'dining', 'amount': -200, 'date': '2024-03-14'}, + {'category': 'entertainment', 'amount': -100, 'date': '2024-03-15'}, + ] + + @pytest.fixture + def sample_budget_response(self): + categories = [ + BudgetCategory( + category='groceries', + type='need', + avg_monthly_spent=300, + monthly_budget=280, + recommended_budget=65, + is_essential=True + ), + BudgetCategory( + category='dining', + type='want', + avg_monthly_spent=200, + monthly_budget=150, + recommended_budget=35, + is_essential=False + ), + BudgetCategory( + category='entertainment', + type='want', + avg_monthly_spent=150, + monthly_budget=120, + recommended_budget=28, + is_essential=False + ), + ] + return BudgetResponse( + budget=categories, + generated_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ) + + # Synchronous tests + def test_get_period_boundaries_weekly(self, performance_calc): + ref_date = pd.Timestamp('2024-03-15') + start, end = performance_calc.get_period_boundaries('w', ref_date) + assert start.strftime('%Y-%m-%d') == '2024-03-10' + assert end.strftime('%Y-%m-%d') == '2024-03-16' + + def test_get_period_boundaries_monthly(self, performance_calc): + ref_date = pd.Timestamp('2024-03-15') + start, end = performance_calc.get_period_boundaries('m', ref_date) + assert start.strftime('%Y-%m-%d') == '2024-03-01' + assert end.strftime('%Y-%m-%d') == '2024-03-31' + + def test_get_period_boundaries_yearly(self, performance_calc): + ref_date = pd.Timestamp('2024-03-15') + start, end = performance_calc.get_period_boundaries('y', ref_date) + assert start.strftime('%Y-%m-%d') == '2024-01-01' + assert end.strftime('%Y-%m-%d') == '2024-12-31' + + def test_calculate_actual_spending(self, performance_calc, mock_transactions): + result = performance_calc.calculate_actual_spending(mock_transactions) + assert result['groceries'] == 450 + assert result['dining'] == 200 + assert result['entertainment'] == 100 + + # Async tests with proper mocking + async def test_generate_performance( + self, + performance_calc, + sample_budget_response, + mock_transactions + ): + """Test full performance generation with proper mocking.""" + user_id = 123 + period = 'w' + + # Mock the database function at the EXACT import path used in budgetPerfCalc + with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: + # Configure the mock to return our test data + mock_get.return_value = mock_transactions + + # Call the async function + result = await performance_calc.generate_performance( + user_id, + sample_budget_response, + period + ) + + # Assertions + assert 'categories' in result + assert 'budgetAmounts' in result + assert 'actualAmounts' in result + assert len(result['categories']) == len(sample_budget_response.budget) + + # Verify the mock was called with correct arguments + mock_get.assert_called_once_with(user_id, ANY, ANY) + + async def test_generate_performance_empty_transactions( + self, + performance_calc, + sample_budget_response + ): + """Test performance generation with no transactions.""" + user_id = 123 + period = 'w' + + with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: + mock_get.return_value = [] + + result = await performance_calc.generate_performance( + user_id, + sample_budget_response, + period + ) + + # All actual amounts should be 0 + assert all(amount == 0 for amount in result['actualAmounts']) + mock_get.assert_called_once() + + async def test_generate_performance_db_error( + self, + performance_calc, + sample_budget_response + ): + """Test performance generation with database error.""" + user_id = 123 + period = 'w' + + with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: + mock_get.side_effect = Exception("Database connection failed") + + # The error should propagate + with pytest.raises(Exception, match="Database connection failed"): + await performance_calc.generate_performance( + user_id, + sample_budget_response, + period + ) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_budget_integration.py b/app/server/services/python/analytics/test/test_budget_integration.py new file mode 100644 index 0000000..263fdc1 --- /dev/null +++ b/app/server/services/python/analytics/test/test_budget_integration.py @@ -0,0 +1,39 @@ +import pytest +from unittest.mock import patch, AsyncMock +from src.logic.budget import generate_budget, generate_budget_performance +from src.models.schemas import BudgetResponse + +class TestBudgetIntegration: + + @pytest.mark.asyncio + async def test_full_budget_flow(self): + """Test the complete budget generation and performance flow.""" + user_id = 123 + period = 'w' + + # Mock transactions for generate_budget + mock_transactions = [ + {'amount': 5000, 'category': 'salary', 'date': '2024-01-01'}, + {'amount': -300, 'category': 'groceries', 'date': '2024-01-02'}, + {'amount': -200, 'category': 'dining', 'date': '2024-01-03'}, + {'amount': 5000, 'category': 'salary', 'date': '2024-02-01'}, + {'amount': -250, 'category': 'groceries', 'date': '2024-02-02'}, + ] + + with patch( + 'src.logic.budget.get_user_transactions_with_connection', + return_value=mock_transactions + ): + # Generate budget + budget = generate_budget(period, user_id) + + assert isinstance(budget, BudgetResponse) + assert len(budget.budget) > 0 + + # Generate performance + performance = await generate_budget_performance(user_id, budget, period) + + assert 'categories' in performance + assert 'budgetAmounts' in performance + assert 'actualAmounts' in performance + assert len(performance['categories']) == len(budget.budget) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_cat_classifier.py b/app/server/services/python/analytics/test/test_cat_classifier.py new file mode 100644 index 0000000..53a262c --- /dev/null +++ b/app/server/services/python/analytics/test/test_cat_classifier.py @@ -0,0 +1,62 @@ +import pytest +from src.utils.trans_cat_classifier import CategoryClassifier + +class TestCategoryClassifier: + + def test_classify_needs(self, classifier): + """Test classification of needs categories.""" + assert classifier.classify('rent') == 'need' + assert classifier.classify('groceries') == 'need' + assert classifier.classify('healthcare') == 'need' + assert classifier.classify('utilities') == 'need' + + def test_classify_wants(self, classifier): + """Test classification of wants categories.""" + assert classifier.classify('dining') == 'want' + assert classifier.classify('entertainment') == 'want' + assert classifier.classify('shopping') == 'want' + assert classifier.classify('coffee') == 'want' + + def test_classify_savings(self, classifier): + """Test classification of savings categories.""" + assert classifier.classify('savings') == 'savings' + assert classifier.classify('investment') == 'savings' + assert classifier.classify('retirement') == 'savings' + + def test_classify_unknown_defaults_to_want(self, classifier): + """Test unknown categories default to 'want'.""" + assert classifier.classify('unknown_category') == 'want' + assert classifier.classify('random_stuff') == 'want' + + def test_classify_case_insensitive(self, classifier): + """Test classification is case insensitive.""" + assert classifier.classify('RENT') == 'need' + assert classifier.classify('Groceries') == 'need' + assert classifier.classify('DINING') == 'want' + + def test_classify_many(self, classifier): + """Test classifying multiple categories at once.""" + categories = {'rent', 'dining', 'savings', 'unknown'} + result = classifier.classify_many(categories) + + assert result['rent'] == 'need' + assert result['dining'] == 'want' + assert result['savings'] == 'savings' + assert result['unknown'] == 'want' + + def test_get_categories_by_type(self, classifier): + """Test filtering categories by type.""" + category_types = { + 'rent': 'need', + 'groceries': 'need', + 'dining': 'want', + 'savings': 'savings' + } + + needs = classifier.get_categories_by_type(category_types, 'need') + wants = classifier.get_categories_by_type(category_types, 'want') + savings = classifier.get_categories_by_type(category_types, 'savings') + + assert needs == {'rent', 'groceries'} + assert wants == {'dining'} + assert savings == {'savings'} \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_dependencies.py b/app/server/services/python/analytics/test/test_dependencies.py new file mode 100644 index 0000000..f236da8 --- /dev/null +++ b/app/server/services/python/analytics/test/test_dependencies.py @@ -0,0 +1,68 @@ +import pytest +from unittest.mock import patch, MagicMock +import mysql.connector +from src.dependencies import get_db_connection, get_current_user +from fastapi import HTTPException +import os +import jwt +from jwt import InvalidSignatureError + +class TestDatabaseConnection: + + @patch('mysql.connector.connect') + def test_get_db_connection_success(self, mock_connect, mock_env_vars): + mock_connection = MagicMock() + mock_connect.return_value = mock_connection + + result = get_db_connection() + + mock_connect.assert_called_once_with( + host='localhost', + user='test_user', + password='test_password', + database='test_db' + ) + + assert result == mock_connection + + @patch('mysql.connector.connect') + def test_get_db_connection_missing_env_vars(self, mock_connect): + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(HTTPException): + get_db_connection() + + @patch('mysql.connector.connect') + def test_get_db_connection_failure(self, mock_connect): + mock_connect.side_effect = mysql.connector.Error("Connection failed") + with patch.dict(os.environ, {'MYSQL_HOST': 'localhost', 'MYSQL_USER': 'test_user', 'MYSQL_PASSWORD': 'test_password', 'DB_NAME': 'test_db'}, clear=True): + with pytest.raises(mysql.connector.Error): + get_db_connection() + + + +class TestJWTAuthentication: + + @pytest.mark.asyncio + async def test_get_current_user_missing_user_id(self): + payload = {'email': 'test@example.com'} + + token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') + + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): + with pytest.raises(HTTPException) as exc_info: + await get_current_user(authorization=f"Bearer {token}") + + assert exc_info.value.status_code == 401 + assert exc_info.value.detail == "Invalid token" + + @pytest.mark.asyncio + async def test_get_current_user_invalid_signature(self): + payload = {'sub': '123'} + token = jwt.encode(payload, 'wrong_secret', algorithm='HS256') + + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): + with pytest.raises(HTTPException) as exc_info: + await get_current_user(authorization=f"Bearer {token}") + + assert exc_info.value.status_code == 401 + assert "signature" in exc_info.value.detail.lower() or "invalid" in exc_info.value.detail.lower() \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_incomeflow.py b/app/server/services/python/analytics/test/test_incomeflow.py new file mode 100644 index 0000000..a7b9125 --- /dev/null +++ b/app/server/services/python/analytics/test/test_incomeflow.py @@ -0,0 +1,136 @@ +# tests/test_service_incomeflow.py +import pytest +import pandas as pd +from src.logic.incomeflow import build_sankey_data + +class TestIncomeflowService: + + def test_build_sankey_data_with_income_and_expenses(self, mock_incomeflow_transactions): + """Test building sankey data with both income and expenses.""" + result = build_sankey_data(mock_incomeflow_transactions) + + assert 'nodes' in result + assert 'links' in result + + # Should have total income node + node_names = [node['name'] for node in result['nodes']] + assert 'total income' in node_names + + # Should have all unique categories as nodes + expected_categories = {'salary', 'freelance', 'interest', 'rent', + 'groceries', 'dining', 'entertainment', 'utilities'} + for category in expected_categories: + assert category in node_names + + # Should have links + assert len(result['links']) > 0 + + # Verify link structure + for link in result['links']: + assert 'source' in link + assert 'target' in link + assert 'value' in link + assert isinstance(link['value'], int) + + def test_build_sankey_data_with_overflow(self): + """Test sankey data when income exceeds expenses.""" + transactions = [ + {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, + {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, + {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, + {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'}, + ] + + result = build_sankey_data(transactions) + + # Should have 'unspent' node + node_names = [node['name'] for node in result['nodes']] + assert 'unspent' in node_names + + # Should have link to unspent + unspent_index = node_names.index('unspent') + total_income_index = node_names.index('total income') + + # Find link from total income to unspent + unspent_links = [link for link in result['links'] + if link['source'] == total_income_index + and link['target'] == unspent_index] + assert len(unspent_links) == 1 + assert unspent_links[0]['value'] == 4000 # 6000 - 2000 + + def test_build_sankey_data_with_overspending(self): + """Test sankey data when expenses exceed income.""" + transactions = [ + {'amount': 3000, 'category': 'salary', 'date': '2024-03-01'}, + {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, + {'amount': -1000, 'category': 'groceries', 'date': '2024-03-04'}, + {'amount': -800, 'category': 'dining', 'date': '2024-03-05'}, + ] + + result = build_sankey_data(transactions) + + # Should have 'overspent' and 'savings' nodes + node_names = [node['name'] for node in result['nodes']] + assert 'overspent' in node_names + assert 'savings' in node_names + + # Should have link from savings to overspent + savings_index = node_names.index('savings') + overspent_index = node_names.index('overspent') + + overspent_links = [link for link in result['links'] + if link['source'] == savings_index + and link['target'] == overspent_index] + assert len(overspent_links) == 1 + assert overspent_links[0]['value'] == 300 # 3300 - 3000 + + def test_build_sankey_data_empty_transactions(self): + """Test sankey data with no transactions.""" + result = build_sankey_data([]) + + assert result['nodes'] == [{'name': 'total income'}, {'name': 'no data'}] + assert result['links'] == [] + + def test_build_sankey_data_only_income(self): + """Test sankey data with only income transactions.""" + transactions = [ + {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, + {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, + ] + + result = build_sankey_data(transactions) + + node_names = [node['name'] for node in result['nodes']] + assert 'unspent' in node_names + + # All income should flow to unspent + total_income_index = node_names.index('total income') + unspent_index = node_names.index('unspent') + + unspent_links = [link for link in result['links'] + if link['source'] == total_income_index + and link['target'] == unspent_index] + assert unspent_links[0]['value'] == 6000 + + def test_build_sankey_data_only_expenses(self): + """Test sankey data with only expense transactions.""" + transactions = [ + {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, + {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'}, + ] + + result = build_sankey_data(transactions) + + node_names = [node['name'] for node in result['nodes']] + assert 'overspent' in node_names + assert 'savings' in node_names + + # Expenses should come from savings + savings_index = node_names.index('savings') + overspent_index = node_names.index('overspent') + rent_index = node_names.index('rent') + groceries_index = node_names.index('groceries') + + # Links from savings to overspent and to categories + savings_links = [link for link in result['links'] if link['source'] == savings_index] + assert len(savings_links) == 1 # savings into overspent \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_periodCalc.py b/app/server/services/python/analytics/test/test_periodCalc.py new file mode 100644 index 0000000..06484b0 --- /dev/null +++ b/app/server/services/python/analytics/test/test_periodCalc.py @@ -0,0 +1,149 @@ +import pytest +import pandas as pd +from src.utils.dates import period_calc + +class TestPeriodCalc: + + def test_period_calc_weekly(self): + end_date = pd.Timestamp('2024-03-15') + + start_date, freq, date_format, period_name = period_calc('w', end_date) + + # Check start date is 7 days before end date + expected_start = end_date - pd.Timedelta(days=7) + assert start_date == expected_start + assert start_date.strftime('%Y-%m-%d') == '2024-03-08' + + # Check other return values + assert freq == 'D' + assert date_format == '%Y-%m-%d' + assert period_name == 'Weekly' + + def test_period_calc_monthly(self): + end_date = pd.Timestamp('2024-03-15') + + start_date, freq, date_format, period_name = period_calc('m', end_date) + + # Check start date is 30 days before end date + expected_start = end_date - pd.Timedelta(days=30) + assert start_date == expected_start + assert start_date.strftime('%Y-%m-%d') == '2024-02-14' # March 15 - 30 days = Feb 14 + + # Check other return values + assert freq == 'D' + assert date_format == '%Y-%m-%d' + assert period_name == 'Monthly' + + def test_period_calc_yearly(self): + end_date = pd.Timestamp('2024-03-15') + + start_date, freq, date_format, period_name = period_calc('y', end_date) + + # Check start date is 365 days before end date + expected_start = end_date - pd.Timedelta(days=365) + assert start_date == expected_start + assert start_date.strftime('%Y-%m-%d') == '2023-03-16' # 2024 is leap year + + # Check other return values + assert freq == 'M' + assert date_format == '%Y-%m' + assert period_name == 'Yearly' + + def test_period_calc_with_different_end_dates(self): + test_cases = [ + # (end_date, expected_weekly_start, expected_monthly_start, expected_yearly_start) + ('2024-01-01', '2023-12-25', '2023-12-02', '2023-01-01'), + ('2024-06-15', '2024-06-08', '2024-05-16', '2023-06-16'), + ('2024-12-31', '2024-12-24', '2024-12-01', '2024-01-01'), + ] + + for end_str, expected_weekly, expected_monthly, expected_yearly in test_cases: + end_date = pd.Timestamp(end_str) + + # Test weekly + start, freq, fmt, name = period_calc('w', end_date) + assert start.strftime('%Y-%m-%d') == expected_weekly + + # Test monthly + start, freq, fmt, name = period_calc('m', end_date) + assert start.strftime('%Y-%m-%d') == expected_monthly + + # Test yearly + start, freq, fmt, name = period_calc('y', end_date) + assert start.strftime('%Y-%m-%d') == expected_yearly + + def test_period_calc_edge_cases(self): + + end_date = pd.Timestamp('2024-02-29') # Leap year + start, _, _, _ = period_calc('m', end_date) + assert start.strftime('%Y-%m-%d') == '2024-01-30' # Feb 29 - 30 days = Jan 30 + + end_date = pd.Timestamp('2024-12-31') + start, _, _, _ = period_calc('y', end_date) + assert start.strftime('%Y-%m-%d') == '2024-01-01' + + end_date = pd.Timestamp('2024-01-01') + start, _, _, _ = period_calc('m', end_date) + assert start.strftime('%Y-%m-%d') == '2023-12-02' # Jan 1 - 30 days = Dec 2 + + def test_period_calc_invalid_period(self): + end_date = pd.Timestamp('2024-03-15') + + # Invalid period should go to else branch (yearly) + start, freq, date_format, period_name = period_calc('invalid', end_date) + + expected_start = end_date - pd.Timedelta(days=365) + assert start == expected_start + assert freq == 'M' + assert date_format == '%Y-%m' + assert period_name == 'Yearly' + + def test_period_calc_with_time_component(self): + """Test that function handles timestamps with time components.""" + end_date = pd.Timestamp('2024-03-15 14:30:45') + + start, _, _, _ = period_calc('w', end_date) + + # Time component should be preserved in the calculation + expected_start = end_date - pd.Timedelta(days=7) + assert start == expected_start + assert start.hour == 14 + assert start.minute == 30 + assert start.second == 45 + + def test_period_calc_returns_tuple(self): + """Test that function returns the correct tuple type.""" + end_date = pd.Timestamp('2024-03-15') + result = period_calc('w', end_date) + + assert isinstance(result, tuple) + assert len(result) == 4 + + start, freq, date_format, period_name = result + assert isinstance(start, pd.Timestamp) + assert isinstance(freq, str) + assert isinstance(date_format, str) + assert isinstance(period_name, str) + + @pytest.mark.parametrize("period,expected_freq,expected_format,expected_name", [ + ('w', 'D', '%Y-%m-%d', 'Weekly'), + ('m', 'D', '%Y-%m-%d', 'Monthly'), + ('y', 'M', '%Y-%m', 'Yearly'), + ]) + def test_period_calc_parametrized(self, period, expected_freq, expected_format, expected_name): + """Parametrized test for all periods.""" + end_date = pd.Timestamp('2024-03-15') + + start, freq, date_format, period_name = period_calc(period, end_date) + + assert freq == expected_freq + assert date_format == expected_format + assert period_name == expected_name + + # Verify start date calculation + if period == 'w': + assert start == end_date - pd.Timedelta(days=7) + elif period == 'm': + assert start == end_date - pd.Timedelta(days=30) + else: + assert start == end_date - pd.Timedelta(days=365) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_savings.py b/app/server/services/python/analytics/test/test_savings.py new file mode 100644 index 0000000..d3ad982 --- /dev/null +++ b/app/server/services/python/analytics/test/test_savings.py @@ -0,0 +1,104 @@ +import pytest +import pandas as pd +from datetime import datetime +from unittest.mock import patch, MagicMock +from src.logic.savings import calculate_savings_over_time + +class TestSavingsService: + + def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_savings_transactions): + """Test savings calculation for weekly period.""" + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=7) + + result = calculate_savings_over_time( + mock_savings_accounts, + mock_savings_transactions, + start_date, + end_date, + 'w', + 'Weekly' + ) + + assert 'labels' in result + assert 'datasets' in result + assert len(result['labels']) == 8 # 7 days + 1? Check your implementation + assert result['datasets'][0]['label'] == 'Weekly Savings' + + # Verify data is numeric + for value in result['datasets'][0]['data']: + assert isinstance(value, (int, float)) + + def test_calculate_savings_over_time_monthly(self, mock_savings_accounts, mock_savings_transactions): + """Test savings calculation for monthly period.""" + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=30) + + result = calculate_savings_over_time( + mock_savings_accounts, + mock_savings_transactions, + start_date, + end_date, + 'm', + 'Monthly' + ) + + assert result['datasets'][0]['label'] == 'Monthly Savings' + assert len(result['labels']) > 0 + + def test_calculate_savings_over_time_yearly(self, mock_savings_accounts, mock_savings_transactions): + """Test savings calculation for yearly period.""" + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=365) + + result = calculate_savings_over_time( + mock_savings_accounts, + mock_savings_transactions, + start_date, + end_date, + 'y', + 'Yearly' + ) + + assert result['datasets'][0]['label'] == 'Yearly Savings' + # Yearly should have monthly labels + assert all(len(label) == 7 for label in result['labels']) # YYYY-MM format + + def test_calculate_savings_over_time_empty_transactions(self, mock_savings_accounts): + """Test savings calculation with no transactions.""" + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=7) + + result = calculate_savings_over_time( + mock_savings_accounts, + [], # Empty transactions + start_date, + end_date, + 'w', + 'Weekly' + ) + + # Should still return structure with zero data + assert 'labels' in result + assert 'datasets' in result + # All savings values should be the initial balances + expected_initial = sum(acc['balance'] for acc in mock_savings_accounts) + assert all(value == expected_initial for value in result['datasets'][0]['data']) + + def test_calculate_savings_over_time_no_accounts(self): + """Test savings calculation with no accounts.""" + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=7) + + result = calculate_savings_over_time( + [], # Empty accounts + [], + start_date, + end_date, + 'w', + 'Weekly' + ) + + # Should return empty structure + assert result['labels'] == [] + assert result['datasets'][0]['data'] == [] \ No newline at end of file diff --git a/app/server/services/python/analytics/tests/test_budget.py b/app/server/services/python/analytics/tests/test_budget.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/server/services/python/analytics/tests/test_incomeflow.py b/app/server/services/python/analytics/tests/test_incomeflow.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/server/services/python/analytics/tests/test_savings.py b/app/server/services/python/analytics/tests/test_savings.py deleted file mode 100644 index e69de29..0000000 From a09ba13137cb8528e27b9e78fec6a5a2caebd79b Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 12:44:56 -0600 Subject: [PATCH 171/204] docker tweaks for tests --- app/server/docker-compose.test.yml | 4 ++-- .../services/python/analytics/Dockerfile.test | 17 +++++++++++------ .../services/python/analytics/requirements.txt | 4 ++++ .../python/analytics/src/dependencies.py | 4 +++- .../python/analytics/src/logic/savings.py | 2 +- .../services/python/analytics/test/conftest.py | 11 +---------- .../python/analytics/test/test_budgetCalc.py | 4 ---- .../analytics/test/test_budgetPerfCalc.py | 3 --- .../analytics/test/test_budget_integration.py | 1 - .../analytics/test/test_cat_classifier.py | 7 ------- .../python/analytics/test/test_dependencies.py | 16 ++++++++++++---- .../python/analytics/test/test_incomeflow.py | 5 ----- .../python/analytics/test/test_periodCalc.py | 3 --- .../python/analytics/test/test_savings.py | 7 +------ 14 files changed, 35 insertions(+), 53 deletions(-) diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 5f310c4..b0ce0aa 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -16,5 +16,5 @@ services: analytics-test: build: - context: services/python - dockerfile: analytics/Dockerfile.test + context: services/python/analytics + dockerfile: Dockerfile.test diff --git a/app/server/services/python/analytics/Dockerfile.test b/app/server/services/python/analytics/Dockerfile.test index 25b3692..cc722a0 100644 --- a/app/server/services/python/analytics/Dockerfile.test +++ b/app/server/services/python/analytics/Dockerfile.test @@ -1,12 +1,17 @@ FROM measureonecodetwice/finus-analytics:local -COPY analytics/test ./test -COPY analytics/requirements-test.txt . +WORKDIR /app/server/services/python/analytics -RUN pip install -r requirements-test.txt +COPY test ./test +COPY requirements.txt . + +RUN pip install -r requirements.txt + +ENV PYTHONPATH=/app/server/services/python/analytics +ENV JWT_SECRET=test_secret_key_12345 +ENV CI=true + +CMD ["python", "-m", "pytest", "test/", "-v", "--cov=src"] -WORKDIR /app -ENV PYTHONPATH=/app -CMD ["pytest", "test/", "-v", "--cov=src", "--cov-report=term-missing", "--cov-report=xml:coverage.xml"] \ No newline at end of file diff --git a/app/server/services/python/analytics/requirements.txt b/app/server/services/python/analytics/requirements.txt index 537ad9f..8b65c9c 100644 --- a/app/server/services/python/analytics/requirements.txt +++ b/app/server/services/python/analytics/requirements.txt @@ -6,3 +6,7 @@ numpy==1.26.3 mysql-connector-python==8.0.33 dotenv==0.9.9 pyjwt==2.11.0 +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.1.0 +pytest-mock>=3.11.0 \ No newline at end of file diff --git a/app/server/services/python/analytics/src/dependencies.py b/app/server/services/python/analytics/src/dependencies.py index 4bbad18..ae26603 100644 --- a/app/server/services/python/analytics/src/dependencies.py +++ b/app/server/services/python/analytics/src/dependencies.py @@ -42,4 +42,6 @@ async def get_current_user(authorization: Optional[str] = Header(None)): except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token") except ValueError: - raise HTTPException(status_code=401, detail="Invalid authorization header format") \ No newline at end of file + raise HTTPException(status_code=401, detail="Invalid authorization header format") + except TypeError as e: + raise HTTPException(status_code=401, detail=str(e)) \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/savings.py b/app/server/services/python/analytics/src/logic/savings.py index f3c4387..b70a5a4 100644 --- a/app/server/services/python/analytics/src/logic/savings.py +++ b/app/server/services/python/analytics/src/logic/savings.py @@ -35,7 +35,7 @@ def calculate_savings_over_time( else: df['year_month'] = df['date'].dt.to_period('M') monthly_groups = df.groupby('year_month') - for month in pd.date_range(start=start_date, end=end_date, freq='ME'): + for month in pd.date_range(start=start_date, end=end_date, freq='M'): month_str = month.strftime('%Y-%m') month_period = pd.Period(month_str, freq='M') if month_period in monthly_groups.groups: diff --git a/app/server/services/python/analytics/test/conftest.py b/app/server/services/python/analytics/test/conftest.py index d06cbc4..67187ae 100644 --- a/app/server/services/python/analytics/test/conftest.py +++ b/app/server/services/python/analytics/test/conftest.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import patch +from unittest.mock import patch, MagicMock from datetime import datetime, timedelta from src.models.schemas import BudgetCategory, BudgetResponse from src.utils.trans_cat_classifier import CategoryClassifier @@ -8,7 +8,6 @@ @pytest.fixture def sample_transactions(): - """Provide sample transaction data.""" base_date = datetime.now() - timedelta(days=30) return [ {'amount': 5000, 'category': 'salary', 'date': base_date}, @@ -65,7 +64,6 @@ def mock_db_queries(): @pytest.fixture def mock_savings_accounts(): - """Mock savings accounts data.""" return [ {'id': 1, 'balance': 5000}, {'id': 2, 'balance': 3000}, @@ -73,7 +71,6 @@ def mock_savings_accounts(): @pytest.fixture def mock_savings_transactions(): - """Mock transactions for savings accounts.""" base_date = datetime.now() - timedelta(days=30) return [ {'financialAccount_id': 1, 'amount': 100, 'date': base_date + timedelta(days=5)}, @@ -85,7 +82,6 @@ def mock_savings_transactions(): @pytest.fixture def mock_incomeflow_transactions(): - """Mock transactions for income flow (mix of income and expenses).""" base_date = datetime.now() - timedelta(days=30) return [ # Income transactions @@ -103,7 +99,6 @@ def mock_incomeflow_transactions(): @pytest.fixture def mock_env_vars(): - """Mock environment variables for testing.""" with patch.dict(os.environ, { 'JWT_SECRET': 'test_secret_key_12345', 'MYSQL_HOST': 'localhost', @@ -115,25 +110,21 @@ def mock_env_vars(): @pytest.fixture def valid_token(): - """Generate a valid JWT token for testing.""" payload = {'sub': '123', 'user_id': '123', 'id': '123'} return jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') @pytest.fixture def expired_token(): - """Generate an expired JWT token.""" import time payload = {'sub': '123', 'exp': int(time.time()) - 3600} return jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') @pytest.fixture def invalid_token(): - """Generate an invalid JWT token.""" return "invalid.token.string" @pytest.fixture def mock_db_connection(): - """Mock database connection.""" mock_conn = MagicMock() mock_cursor = MagicMock() mock_conn.cursor.return_value = mock_cursor diff --git a/app/server/services/python/analytics/test/test_budgetCalc.py b/app/server/services/python/analytics/test/test_budgetCalc.py index fd89755..87d6565 100644 --- a/app/server/services/python/analytics/test/test_budgetCalc.py +++ b/app/server/services/python/analytics/test/test_budgetCalc.py @@ -12,7 +12,6 @@ def calculator(self): return BudgetCalculator(classifier) def test_separate_income_expenses(self, calculator, sample_transactions): - """Test separating income and expenses.""" income, expenses = calculator.separate_income_expenses(sample_transactions) assert len(income) == 3 # Three positive transactions @@ -62,7 +61,6 @@ def test_calculate_budget_pools(self, calculator): assert pools['savings'] == 1000 # 20% def test_calculate_category_budget_need(self, calculator): - """Test budget calculation for need category.""" monthly_avg = {'rent': 1200, 'groceries': 400} category_types = {'rent': 'need', 'groceries': 'need'} pools = {'needs': 2000, 'wants': 1000, 'savings': 500} @@ -75,7 +73,6 @@ def test_calculate_category_budget_need(self, calculator): assert result == 1500 def test_calculate_category_budget_want(self, calculator): - """Test budget calculation for want category.""" monthly_avg = {'dining': 500, 'entertainment': 300} category_types = {'dining': 'want', 'entertainment': 'want'} pools = {'needs': 2000, 'wants': 1000, 'savings': 500} @@ -88,7 +85,6 @@ def test_calculate_category_budget_want(self, calculator): assert result == 625 def test_scale_budget(self, calculator): - """Test budget scaling for different periods.""" monthly_amount = 1000 weekly = calculator.scale_budget(monthly_amount, 'w') diff --git a/app/server/services/python/analytics/test/test_budgetPerfCalc.py b/app/server/services/python/analytics/test/test_budgetPerfCalc.py index e267b59..dfa3e17 100644 --- a/app/server/services/python/analytics/test/test_budgetPerfCalc.py +++ b/app/server/services/python/analytics/test/test_budgetPerfCalc.py @@ -87,7 +87,6 @@ async def test_generate_performance( sample_budget_response, mock_transactions ): - """Test full performance generation with proper mocking.""" user_id = 123 period = 'w' @@ -117,7 +116,6 @@ async def test_generate_performance_empty_transactions( performance_calc, sample_budget_response ): - """Test performance generation with no transactions.""" user_id = 123 period = 'w' @@ -139,7 +137,6 @@ async def test_generate_performance_db_error( performance_calc, sample_budget_response ): - """Test performance generation with database error.""" user_id = 123 period = 'w' diff --git a/app/server/services/python/analytics/test/test_budget_integration.py b/app/server/services/python/analytics/test/test_budget_integration.py index 263fdc1..56724c3 100644 --- a/app/server/services/python/analytics/test/test_budget_integration.py +++ b/app/server/services/python/analytics/test/test_budget_integration.py @@ -7,7 +7,6 @@ class TestBudgetIntegration: @pytest.mark.asyncio async def test_full_budget_flow(self): - """Test the complete budget generation and performance flow.""" user_id = 123 period = 'w' diff --git a/app/server/services/python/analytics/test/test_cat_classifier.py b/app/server/services/python/analytics/test/test_cat_classifier.py index 53a262c..ef7cb27 100644 --- a/app/server/services/python/analytics/test/test_cat_classifier.py +++ b/app/server/services/python/analytics/test/test_cat_classifier.py @@ -4,38 +4,32 @@ class TestCategoryClassifier: def test_classify_needs(self, classifier): - """Test classification of needs categories.""" assert classifier.classify('rent') == 'need' assert classifier.classify('groceries') == 'need' assert classifier.classify('healthcare') == 'need' assert classifier.classify('utilities') == 'need' def test_classify_wants(self, classifier): - """Test classification of wants categories.""" assert classifier.classify('dining') == 'want' assert classifier.classify('entertainment') == 'want' assert classifier.classify('shopping') == 'want' assert classifier.classify('coffee') == 'want' def test_classify_savings(self, classifier): - """Test classification of savings categories.""" assert classifier.classify('savings') == 'savings' assert classifier.classify('investment') == 'savings' assert classifier.classify('retirement') == 'savings' def test_classify_unknown_defaults_to_want(self, classifier): - """Test unknown categories default to 'want'.""" assert classifier.classify('unknown_category') == 'want' assert classifier.classify('random_stuff') == 'want' def test_classify_case_insensitive(self, classifier): - """Test classification is case insensitive.""" assert classifier.classify('RENT') == 'need' assert classifier.classify('Groceries') == 'need' assert classifier.classify('DINING') == 'want' def test_classify_many(self, classifier): - """Test classifying multiple categories at once.""" categories = {'rent', 'dining', 'savings', 'unknown'} result = classifier.classify_many(categories) @@ -45,7 +39,6 @@ def test_classify_many(self, classifier): assert result['unknown'] == 'want' def test_get_categories_by_type(self, classifier): - """Test filtering categories by type.""" category_types = { 'rent': 'need', 'groceries': 'need', diff --git a/app/server/services/python/analytics/test/test_dependencies.py b/app/server/services/python/analytics/test/test_dependencies.py index f236da8..df09d1d 100644 --- a/app/server/services/python/analytics/test/test_dependencies.py +++ b/app/server/services/python/analytics/test/test_dependencies.py @@ -44,25 +44,33 @@ class TestJWTAuthentication: @pytest.mark.asyncio async def test_get_current_user_missing_user_id(self): - payload = {'email': 'test@example.com'} - + + payload = {'email': 'test@example.com'} token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') + # Ensure token is string + if isinstance(token, bytes): + token = token.decode('utf-8') + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): with pytest.raises(HTTPException) as exc_info: await get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401 - assert exc_info.value.detail == "Invalid token" + assert exc_info.value.detail == "User ID not found in token" @pytest.mark.asyncio async def test_get_current_user_invalid_signature(self): + payload = {'sub': '123'} token = jwt.encode(payload, 'wrong_secret', algorithm='HS256') + if isinstance(token, bytes): + token = token.decode('utf-8') + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): with pytest.raises(HTTPException) as exc_info: await get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401 - assert "signature" in exc_info.value.detail.lower() or "invalid" in exc_info.value.detail.lower() \ No newline at end of file + assert "token" in exc_info.value.detail.lower() \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_incomeflow.py b/app/server/services/python/analytics/test/test_incomeflow.py index a7b9125..18945bf 100644 --- a/app/server/services/python/analytics/test/test_incomeflow.py +++ b/app/server/services/python/analytics/test/test_incomeflow.py @@ -6,7 +6,6 @@ class TestIncomeflowService: def test_build_sankey_data_with_income_and_expenses(self, mock_incomeflow_transactions): - """Test building sankey data with both income and expenses.""" result = build_sankey_data(mock_incomeflow_transactions) assert 'nodes' in result @@ -33,7 +32,6 @@ def test_build_sankey_data_with_income_and_expenses(self, mock_incomeflow_transa assert isinstance(link['value'], int) def test_build_sankey_data_with_overflow(self): - """Test sankey data when income exceeds expenses.""" transactions = [ {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, @@ -59,7 +57,6 @@ def test_build_sankey_data_with_overflow(self): assert unspent_links[0]['value'] == 4000 # 6000 - 2000 def test_build_sankey_data_with_overspending(self): - """Test sankey data when expenses exceed income.""" transactions = [ {'amount': 3000, 'category': 'salary', 'date': '2024-03-01'}, {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, @@ -92,7 +89,6 @@ def test_build_sankey_data_empty_transactions(self): assert result['links'] == [] def test_build_sankey_data_only_income(self): - """Test sankey data with only income transactions.""" transactions = [ {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, @@ -113,7 +109,6 @@ def test_build_sankey_data_only_income(self): assert unspent_links[0]['value'] == 6000 def test_build_sankey_data_only_expenses(self): - """Test sankey data with only expense transactions.""" transactions = [ {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'}, diff --git a/app/server/services/python/analytics/test/test_periodCalc.py b/app/server/services/python/analytics/test/test_periodCalc.py index 06484b0..8e272e9 100644 --- a/app/server/services/python/analytics/test/test_periodCalc.py +++ b/app/server/services/python/analytics/test/test_periodCalc.py @@ -99,7 +99,6 @@ def test_period_calc_invalid_period(self): assert period_name == 'Yearly' def test_period_calc_with_time_component(self): - """Test that function handles timestamps with time components.""" end_date = pd.Timestamp('2024-03-15 14:30:45') start, _, _, _ = period_calc('w', end_date) @@ -112,7 +111,6 @@ def test_period_calc_with_time_component(self): assert start.second == 45 def test_period_calc_returns_tuple(self): - """Test that function returns the correct tuple type.""" end_date = pd.Timestamp('2024-03-15') result = period_calc('w', end_date) @@ -131,7 +129,6 @@ def test_period_calc_returns_tuple(self): ('y', 'M', '%Y-%m', 'Yearly'), ]) def test_period_calc_parametrized(self, period, expected_freq, expected_format, expected_name): - """Parametrized test for all periods.""" end_date = pd.Timestamp('2024-03-15') start, freq, date_format, period_name = period_calc(period, end_date) diff --git a/app/server/services/python/analytics/test/test_savings.py b/app/server/services/python/analytics/test/test_savings.py index d3ad982..02d7216 100644 --- a/app/server/services/python/analytics/test/test_savings.py +++ b/app/server/services/python/analytics/test/test_savings.py @@ -4,10 +4,9 @@ from unittest.mock import patch, MagicMock from src.logic.savings import calculate_savings_over_time -class TestSavingsService: +class TestSavings: def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_savings_transactions): - """Test savings calculation for weekly period.""" end_date = pd.Timestamp('2024-03-15') start_date = end_date - pd.Timedelta(days=7) @@ -30,7 +29,6 @@ def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_sa assert isinstance(value, (int, float)) def test_calculate_savings_over_time_monthly(self, mock_savings_accounts, mock_savings_transactions): - """Test savings calculation for monthly period.""" end_date = pd.Timestamp('2024-03-15') start_date = end_date - pd.Timedelta(days=30) @@ -47,7 +45,6 @@ def test_calculate_savings_over_time_monthly(self, mock_savings_accounts, mock_s assert len(result['labels']) > 0 def test_calculate_savings_over_time_yearly(self, mock_savings_accounts, mock_savings_transactions): - """Test savings calculation for yearly period.""" end_date = pd.Timestamp('2024-03-15') start_date = end_date - pd.Timedelta(days=365) @@ -65,7 +62,6 @@ def test_calculate_savings_over_time_yearly(self, mock_savings_accounts, mock_sa assert all(len(label) == 7 for label in result['labels']) # YYYY-MM format def test_calculate_savings_over_time_empty_transactions(self, mock_savings_accounts): - """Test savings calculation with no transactions.""" end_date = pd.Timestamp('2024-03-15') start_date = end_date - pd.Timedelta(days=7) @@ -86,7 +82,6 @@ def test_calculate_savings_over_time_empty_transactions(self, mock_savings_accou assert all(value == expected_initial for value in result['datasets'][0]['data']) def test_calculate_savings_over_time_no_accounts(self): - """Test savings calculation with no accounts.""" end_date = pd.Timestamp('2024-03-15') start_date = end_date - pd.Timedelta(days=7) From 55685fd1f969e3b6ddf188cdd25090666f87ec3c Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 13:13:21 -0600 Subject: [PATCH 172/204] uploading unfinished test plan --- documentation/Finus_Test_Plan.docx.md | 139 ++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 documentation/Finus_Test_Plan.docx.md diff --git a/documentation/Finus_Test_Plan.docx.md b/documentation/Finus_Test_Plan.docx.md new file mode 100644 index 0000000..d8ba632 --- /dev/null +++ b/documentation/Finus_Test_Plan.docx.md @@ -0,0 +1,139 @@ + +***Note that you can refine your testing plan as the project development goes. Keep the change log as follow:*** + +*Change-log* + +| Version | Change Date | By | Description | +| :---: | :---: | :---: | :---: | +| version number | Date of Change | Name of person who made changes | Description of the changes made | +| 1.0 | March 2 | Roman Tebel | Initial write up | +| 1.1 | March 5 | Deep Patel | Added more details to the scope, roles, and testing | + +1. # **Introduction** + + 1. ## **Scope** + +These are the features we have implemented for Sprint 2 and have created tests for: + +1\. User Authentication + +1. Sign up +2. Login +3. Logout + +2\. Visualization Dashboard + +1. Savings chart logic +2. Income flow chart logic +3. Budget calculation and performance against expenses +4. Lookup of transactions +5. Aggregation of snapshot data + +3\. User Data Input + +The following are the features to be implemented in future sprints: Financial Goals, Financial Projections, Stock and FOREX Tracking, Collaborative Budgets (stretch), and ML Integration (stretch). + +The scope includes functional verification through unit, integration, acceptance, and regression testing for the features implemented. + +The unit and integration tests will confirm code quality and can potentially evaluate some of the project requirements. A major portion of project requirements in our case must be validated through a manual walkthrough within the client. + +Load testing will be conducted in Sprint 3 in order to ensure that the server can support the expected usage load, specified in the course outline for the project. + +Mutation testing will be conducted in Sprint 3 in order to expand code coverage and make the project more robust. + + + +2. ## **Roles and Responsibilities** + +Each team member is assigned one or more of the following roles: + +1. Frontend Developer +2. Backend Developer +3. DevOps +4. Tester +5. Quality Assurance + +| Name | Net ID | GitHub username | Role | +| :---- | :---- | :---- | :---- | +| Roman Tebel | 7929015 | TebelR | Frontend Developer \+ Backend Developer \+ Tester | +| Logan Decock | 7966258 | MeasureOneCodeTwice | Frontend Developer \+ Backend Developer \+ DevOps | +| Jackie Mei | 7882240 | JackieMei3 | Frontend Developer \+ Backend Developer \+ QA | +| Dylan Prabagaran | 7898220 | dyll87 | Frontend Developer \+ Backend Developer \+ Tester | +| Deep Patel | 7957389 | deep-n-patel | Frontend Developer \+ Backend Developer \+ Tester | +| Hoang Huy Truong | 7960938 | HuyTruong24 | Frontend Developer \+ Backend Developer \+ Tester | + +**Role Details** + +* Frontend Developer + * Build and maintain the user interface of the application. + * Connect the frontend to backend services through APIs. + * Ensure the application is responsive and user-friendly. + * Test and debug frontend code to maintain performance and quality. + +* Backend Developer + * Develop and maintain server-side logic and APIs. + * Manage databases and data processing. + * Implement security, authentication, and business logic. + * Ensure the system is reliable, scalable, and well-tested. + +* DevOps + * Manager docker compose files, environments and secrets + * Create and maintain CI/CD pipelines + * Environment set up such linting and formatting as a pre-commit hooks + +* Tester + * Creates testing plans and strategies + * Responsible for performing both manual and automated testing + * Makes sure that the software meets the performance and quality standards + +* Quality Assurance + * Searches/Finds bugs in development & build process as well as in production. + + +2. # **Test Methodology** + + 1. ## **Automated regression testing** + +**2.1.1 Test Levels** + +Test levels define the Types of Testing to be executed on the Application Under Test (AUT). In this course, unit testing, integration testing, acceptance testing, regression testing, and load testing are mandatory. + +**Requirements**: + +- For unit testing, at least 10 unit tests for EACH core feature to cover the code related to each core feature. +- For integration testing, at least 10 in total to cover core features. +- Acceptance testing for each core feature. Let’s use end-user test for this. You can ask real end-user or your team members to go through each user story and see if the requirements are meet. +- For regression testing, need to execute all above unit tests \+ integration tests you have for each commit pushed to main branch. + + +| Test Level | Scope & Requirement | Methodology (How will you do this?) | +| :---- | :---- | :---- | +| **Unit Testing** | **User Authentication:** 20 tests **User Input: Visualization Dashboard:** 50 in Python, 46 in TypeScript **Total:** | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | +| **Integration Testing** | **10 tests total** covering interactions between features. | *Running tests in their environment with microservices fully operational and communicating with each other.* | +| **Acceptance Testing** | **End-user testing** for every user story. | *Team members/external users will perform Manual Walkthroughs based on User Story criteria. Deficiencies and potential improvements will be documented.* | +| **Regression Testing** | Unit \+ Integration tests executed on **every push to main branch**. | *We have configured a GitHub Actions CI pipeline to run all tests automatically.* | + +**2.1.2 CI/CD Regression Workflows** + +On pull request to any branch github actions builds our docker images and runs our test suite. Additionally, build & test our software on any commit to a feature branch of the form feature/\*, development and main. + +When a commit is pushed to a feature branch, github actions will also build and publish our docker images to docker hub, tagging the images with a release version derived from the branch name. + +2. ## **Mutation Testing (Test effectiveness)** + +*Skip for Sprint 2\.* + +3. ## **Load Testing** + +*Skip for Sprint 2\.* + +3. # **Terms/Acronyms** + +Make a mention of any terms or acronyms used in the project + +| TERM/ACRONYM | DEFINITION | +| :---- | :---- | +| API | Application Program Interface | +| AUT | Application Under Test | +| JWT | JSON Web Token | + From 51d80c14c804b0cec55bfdbf934b3a4d857d2d7d Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Fri, 6 Mar 2026 13:41:37 -0600 Subject: [PATCH 173/204] Minor Fix to where minimum age to sign up for Finus is 16 instead of 1 --- app/server/services/ts/auth/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/server/services/ts/auth/src/config.ts b/app/server/services/ts/auth/src/config.ts index 4918864..0e88bb3 100644 --- a/app/server/services/ts/auth/src/config.ts +++ b/app/server/services/ts/auth/src/config.ts @@ -1,4 +1,4 @@ export const JWT_SECRET = process.env.JWT_SECRET; export const MIN_PASSWORD_LENGTH = 8; -export const MIN_AGE = 1; +export const MIN_AGE = 16; export const MAX_AGE = 120; From 13a8921c10e930a642c3e32918d53fc4696d7e74 Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 6 Mar 2026 13:52:04 -0600 Subject: [PATCH 174/204] Making fixes to transactions components --- app/client/src/components/AccountList.tsx | 4 +- app/client/src/components/AccountListCard.tsx | 6 +-- app/client/src/components/TransactionList.tsx | 15 +++++- app/client/src/components/TransationForm.tsx | 53 ++++++++++++------- app/client/src/utils/ValidateForms.ts | 2 +- app/client/test/validateForms.test.ts | 12 ++--- 6 files changed, 58 insertions(+), 34 deletions(-) diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx index 4e87244..583e10a 100644 --- a/app/client/src/components/AccountList.tsx +++ b/app/client/src/components/AccountList.tsx @@ -4,7 +4,7 @@ import { type Account } from "../types/AccountType"; import AccountListCard from "./AccountListCard"; import AccountPopup from "./AccountForm"; import type { AuthSession } from "@/pages/authTypes"; - +import "./userForm.css"; interface listProp { session: AuthSession; } @@ -56,7 +56,7 @@ export default function AccountList({ session }: listProp) { return ( <> -
+
{userAccounts.map((account) => ( -
-

{account.name}

+
+

{account.name}



diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx index f7bdbc2..66e1067 100644 --- a/app/client/src/components/TransactionList.tsx +++ b/app/client/src/components/TransactionList.tsx @@ -6,7 +6,7 @@ import TransactionCard from "./TransactionCard"; import type { AuthSession } from "@/pages/authTypes"; import { type Account } from "@/types/AccountType"; import { getUserAccounts } from "@/api/Account"; - +import "./userForm.css"; interface listProp { session: AuthSession; } @@ -67,13 +67,23 @@ export default function TransactionList({ session }: listProp) { ); }; + const setTransaction = (editTransaction: Transaction) => { + setAccountTransactions( + accountTransactions.map((transaction) => + transaction.id === editTransaction.id + ? { ...editTransaction } + : transaction, + ), + ); + }; + const toggle = () => { setSeen(!seen); }; return ( <> -
+
- setSelectedAccount(event.target.value.split(",")) - } + value={selectedAccount} + onChange={(event) => { + setSelectedAccount(Number(event.target.value)); + }} > {account && account.map((account) => ( - ))} diff --git a/app/client/src/utils/ValidateForms.ts b/app/client/src/utils/ValidateForms.ts index 146a8ab..10cb245 100644 --- a/app/client/src/utils/ValidateForms.ts +++ b/app/client/src/utils/ValidateForms.ts @@ -34,7 +34,7 @@ export function validateAccountForm( } export function validateTransactionForm( - account_id: string, + account_id: number, transactionType: string, amount: number, date: string, diff --git a/app/client/test/validateForms.test.ts b/app/client/test/validateForms.test.ts index 5633443..512a0ae 100644 --- a/app/client/test/validateForms.test.ts +++ b/app/client/test/validateForms.test.ts @@ -77,7 +77,7 @@ describe("validateTransactionForm", () => { it("returns true when CSV file is provided", () => { const file = mockFile(); const result = validateTransactionForm( - "123", + 123, "Deposit", 100, "2024-01-01", @@ -89,20 +89,20 @@ describe("validateTransactionForm", () => { it("returns true for valid manual transaction input", () => { const validType = Object.values(transactionCategory)[0]; - const result = validateTransactionForm("123", validType, 50, "2024-01-01"); + const result = validateTransactionForm(123, validType, 50, "2024-01-01"); expect(result).toBe(true); }); it("returns false when account_id is missing", () => { const validType = Object.values(transactionCategory)[0]; - const result = validateTransactionForm("", validType, 50, "2024-01-01"); + const result = validateTransactionForm(0, validType, 50, "2024-01-01"); expect(result).toBe(false); }); it("returns false for invalid transaction type", () => { const result = validateTransactionForm( - "123", + 123, "INVALID_TYPE", 50, "2024-01-01", @@ -113,14 +113,14 @@ describe("validateTransactionForm", () => { it("returns false for non-positive amount", () => { const validType = Object.values(transactionCategory)[0]; - const result = validateTransactionForm("123", validType, 0, "2024-01-01"); + const result = validateTransactionForm(123, validType, 0, "2024-01-01"); expect(result).toBe(false); }); it("returns false when date is missing", () => { const validType = Object.values(transactionCategory)[0]; - const result = validateTransactionForm("123", validType, 50, ""); + const result = validateTransactionForm(123, validType, 50, ""); expect(result).toBe(false); }); }); From f2fbc3253ce1b182cde6513b60cfecb6216a59bc Mon Sep 17 00:00:00 2001 From: Roman Tebel Date: Fri, 6 Mar 2026 14:40:41 -0600 Subject: [PATCH 175/204] cleanup and populator rework --- app/client/bun.lock | 8 +++- app/client/package.json | 2 +- app/client/src/api/DashboardAPI.ts | 7 ++-- .../src/components/DashboardChartSection.tsx | 2 + app/server/services/database/populator.py | 42 ++++++++++++------- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/app/client/bun.lock b/app/client/bun.lock index e586bc7..da957ab 100644 --- a/app/client/bun.lock +++ b/app/client/bun.lock @@ -12,6 +12,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", + "papaparse": "^5.5.3", "radix-ui": "^1.4.3", "react": "^19.2.0", "react-chartjs-2": "^5.3.1", @@ -26,6 +27,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^25.2.0", + "@types/papaparse": "^5.3.16", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.2", @@ -37,7 +39,7 @@ "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "npm:rolldown-vite@7.2.5", + "vite": "^7.2.5", }, }, }, @@ -405,6 +407,8 @@ "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="], + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], @@ -733,6 +737,8 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], diff --git a/app/client/package.json b/app/client/package.json index 1042b6b..eaba2b8 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -44,7 +44,7 @@ "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "npm:rolldown-vite@7.2.5" + "vite": "^7.2.5" }, "overrides": { "vite": "npm:rolldown-vite@7.2.5" diff --git a/app/client/src/api/DashboardAPI.ts b/app/client/src/api/DashboardAPI.ts index 5f49121..68e5e6e 100644 --- a/app/client/src/api/DashboardAPI.ts +++ b/app/client/src/api/DashboardAPI.ts @@ -32,7 +32,7 @@ async function getTransactions(): Promise { return output; } catch (error) { - console.error("Error fetching expenses chart data:", error); + console.error("Error fetching transactions data:", error); throw error; } } @@ -106,7 +106,7 @@ async function getExpensesChartData( `Failed to fetch expenses chart data: ${response.statusText}`, ); } - if (!response.data.datasets) { + if (!response.data) { return null; } return { @@ -137,13 +137,14 @@ async function getSavingsContribChartData( ); } const response = await instance.get(`/charts/savings?period=${period}`); + console.log("received savings data", response); if (response.status !== 200) { throw new Error( `Failed to fetch savings contribution chart data: ${response.statusText}`, ); } - if (response && response.data.datasets > 0) { + if (response && response.data.datasets) { return { labels: response.data["labels"], datasets: [ diff --git a/app/client/src/components/DashboardChartSection.tsx b/app/client/src/components/DashboardChartSection.tsx index 012a329..c906ef5 100644 --- a/app/client/src/components/DashboardChartSection.tsx +++ b/app/client/src/components/DashboardChartSection.tsx @@ -49,6 +49,7 @@ function DashboardChartSection() { setIsLoading(true); try { const data = await getTestSavingsContribData(selectedPeriod); + console.log("data", data); setsavingsData(data); } catch (error) { console.error("Failed to fetch savings data:", error); @@ -104,6 +105,7 @@ function DashboardChartSection() { ): Promise | null> => { try { const response = await getSavingsContribChartData(period); + console.log("Received savings contribution chart data:", response); return response; } catch (error) { console.error("Error fetching savings contribution chart data:", error); diff --git a/app/server/services/database/populator.py b/app/server/services/database/populator.py index cabd4bc..88e5629 100644 --- a/app/server/services/database/populator.py +++ b/app/server/services/database/populator.py @@ -28,7 +28,7 @@ def get_db_connection(): # Configuration for mock data generation NUM_USERS = 1 -ACCOUNTS_PER_USER = 3 # Average number of financial accounts per user +ACCOUNTS_PER_USER = 5 # Average number of financial accounts per user TRANSACTIONS_PER_ACCOUNT = 200 START_DATE = datetime.now() - timedelta(days=365) @@ -41,7 +41,7 @@ def get_db_connection(): FINANCIAL_ACCOUNT_TYPES = ['chequing', 'savings', 'credit_card', 'investment'] -FINANCIAL_ACCOUNT_SUBTYPES = ['RRSP', 'TFSA', 'FHSA', 'RESP', 'RDSP', 'na'] +FINANCIAL_ACCOUNT_SUBTYPES = ['RRSP', 'TFSA', 'FHSA', 'RESP', 'RDSP', 'loan', 'na'] INVESTMENT_TYPES = ['stocks', 'bonds', 'mutual funds', 'ETFs'] GOAL_TYPES = ['money', 'debt'] FIRST_NAMES = ['John', 'Jane', 'Alex', 'Emily', 'Michael', 'Sarah', 'David', 'Laura'] @@ -105,24 +105,34 @@ def create_financial_accounts(cursor, profile_ids): non_credit_account_types = [acc in FINANCIAL_ACCOUNT_TYPES for acc in FINANCIAL_ACCOUNT_TYPES if (acc != 'na' or acc != 'loan')] - # randomly assign subtype for savings accounts subtype = None - if acc_type == 'savings' and random.random() > 0.5: - subtype = random.choice(non_credit_account_types) - if acc_type == 'credit_card': - random.choice(['na','loan']) + if acc_type == 'savings': + if random.random() > 0.5: + savings_subtypes = ['RRSP', 'TFSA', 'FHSA', 'RESP', 'RDSP'] + subtype = random.choice(savings_subtypes) - cursor.execute(""" - INSERT INTO finus.financialAccount (id, name, type, balance, value, last_updated, subtype) - VALUES (%s, %s, %s, %s, %s, %s, %s) - """, (financialAccount_id, name, acc_type, balance, value, last_updated, subtype)) + elif acc_type == 'credit_card': + credit_subtypes = ['na', 'loan'] + subtype = random.choice(credit_subtypes) - financialAccount_ids.append(financialAccount_id) + try: + cursor.execute(""" + INSERT INTO finus.financialAccount (id, name, type, balance, value, last_updated, subtype) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, (financialAccount_id, name, acc_type, balance, value, last_updated, subtype)) + + financialAccount_ids.append(financialAccount_id) + + cursor.execute(""" + INSERT INTO finus.profile_financialAccount (profile_id, financialAccount_id) + VALUES (%s, %s) + """, (profile_id, financialAccount_id)) + + except Exception as e: + print(e) - cursor.execute(""" - INSERT INTO finus.profile_financialAccount (profile_id, financialAccount_id) - VALUES (%s, %s) - """, (profile_id, financialAccount_id)) + # financialAccount_ids.append(financialAccount_id) + return financialAccount_ids From c65899a478f0c0cefdd245caf8ec848a946bf393 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 18:37:28 -0600 Subject: [PATCH 176/204] fixed parsing logic for csv parsing --- .../src/components/csvread/CsvPreview.tsx | 2 ++ app/client/src/utils/NormalizeRow.ts | 22 +++++++++---------- app/client/src/utils/ParseCsv.ts | 2 ++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/client/src/components/csvread/CsvPreview.tsx b/app/client/src/components/csvread/CsvPreview.tsx index fd26de9..9af2308 100644 --- a/app/client/src/components/csvread/CsvPreview.tsx +++ b/app/client/src/components/csvread/CsvPreview.tsx @@ -33,6 +33,7 @@ export default function CsvPreviewTable({ rows }: Props) { Amount Sender Recipient + Category Errors @@ -53,6 +54,7 @@ export default function CsvPreviewTable({ rows }: Props) { {row.amount} {row.sender} {row.recipient} + {row.category} {isInvalid ? row.errors.join(", ") : "—"} ); diff --git a/app/client/src/utils/NormalizeRow.ts b/app/client/src/utils/NormalizeRow.ts index f13fd0a..ecd445d 100644 --- a/app/client/src/utils/NormalizeRow.ts +++ b/app/client/src/utils/NormalizeRow.ts @@ -37,24 +37,22 @@ export function normalizeRow(raw: unknown): NormalizedRow { function normalizeDate(input: string | null): string | null { if (!input) return null; + // Replace separators with hyphens const data = input.replace(/[.\s]/g, "-").replace(/\//g, "-"); - // Already normalized + // Already normalized YYYY-MM-DD if (/^\d{4}-\d{2}-\d{2}$/.test(data)) return data; - // Matches DD-MM-YYYY or MM-DD-YYYY - if (/^\d{2}-\d{2}-\d{4}$/.test(data)) { - const [a, b, y] = data.split("-"); + // Match M-D-YYYY or MM-DD-YYYY + const mdY = /^(\d{1,2})-(\d{1,2})-(\d{4})$/; + const match = data.match(mdY); - if (Number(a) > 12) { - const d = a; - const m = b; - return `${y}-${m}-${d}`; - } + if (match) { + const [, mRaw, dRaw, y] = match; + + const m = mRaw.padStart(2, "0"); + const d = dRaw.padStart(2, "0"); - //assume MM-DD-YYYY - const m = a; - const d = b; return `${y}-${m}-${d}`; } diff --git a/app/client/src/utils/ParseCsv.ts b/app/client/src/utils/ParseCsv.ts index cd3f627..2e5a6b9 100644 --- a/app/client/src/utils/ParseCsv.ts +++ b/app/client/src/utils/ParseCsv.ts @@ -12,6 +12,8 @@ export function parseCsvFile(file: File): Promise { header: true, skipEmptyLines: true, dynamicTyping: true, + beforeFirstChunk: (chunk) => chunk.replace(/^\uFEFF/, ""), + transformHeader: (header) => header.trim().toLowerCase(), complete: (result: ParseResult) => { // handle parsing errors From 46d2e50610142bfcacfe31cfed8a995371dcfc9c Mon Sep 17 00:00:00 2001 From: Jackie Mei Date: Fri, 6 Mar 2026 18:46:58 -0600 Subject: [PATCH 177/204] Tranasction request handling is fixed --- app/client/src/api/Transaction.ts | 32 ++-- app/client/src/components/TransactionCard.tsx | 1 + app/client/src/components/TransactionList.tsx | 6 +- app/client/src/components/TransationForm.tsx | 46 ++++-- app/client/src/types/Transaction.tsx | 4 +- .../services/ts/user/src/routes/account.ts | 27 +-- .../ts/user/src/routes/transaction.ts | 155 ++++++++++++++++-- 7 files changed, 192 insertions(+), 79 deletions(-) diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index aab553d..a6a12ae 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -7,17 +7,16 @@ const requestUrl = "http://localhost:3000/api/transactions"; //Sends a GET request to get the list of user transactions for the account export async function getTransactions( session: AuthSession, - account_id: string, + financialAccount_id: string, ): Promise { try { - //Put as object to convert to json when sent in the body - const content = { id: account_id }; - - const response = await fetch(requestUrl, { - method: "GET", - headers: { Authorization: `Bearer ${session.token}` }, - body: JSON.stringify(content), - }); + const response = await fetch( + `${requestUrl}/?financialAccount_id=${financialAccount_id}`, + { + method: "GET", + headers: { Authorization: `Bearer ${session.token}` }, + }, + ); if (!response.ok) { alert("Failed to retrieve account's transaction\n"); @@ -35,8 +34,8 @@ export async function getTransactions( //Can send multiple transactions in a push request export async function postTranscations( session: AuthSession, - trans: Transaction[], -): Promise { + trans: Transaction, +): Promise { try { const response = await fetch(requestUrl, { method: "POST", @@ -66,7 +65,7 @@ export async function putTranscations( trans: Transaction, ): Promise { try { - const response = await fetch(`${requestUrl}/${trans.id}`, { + const response = await fetch(requestUrl, { method: "PUT", headers: { "content-type": "application/json", @@ -93,11 +92,16 @@ export async function deleteTransaction( selectedTransaction: Transaction, ): Promise { try { - const response = await fetch(`${requestUrl}/${selectedTransaction.id}`, { + const response = await fetch(requestUrl, { method: "DELETE", headers: { + "content-type": "application/json", Authorization: `Bearer ${session.token}`, }, + body: JSON.stringify({ + id: selectedTransaction.id, + financialAccount_id: selectedTransaction.financialAccount_id, + }), }); if (!response.ok) { @@ -114,7 +118,7 @@ export async function deleteTransaction( // POST /api/transactions/csvTransaction export async function uploadCsvTransactions( session: AuthSession, - financialAccount_id: string, + financialAccount_id: number, transactions: Transaction[], ) { try { diff --git a/app/client/src/components/TransactionCard.tsx b/app/client/src/components/TransactionCard.tsx index cc19ec5..33cdc70 100644 --- a/app/client/src/components/TransactionCard.tsx +++ b/app/client/src/components/TransactionCard.tsx @@ -60,6 +60,7 @@ export default function Card({ session={session} edit={true} setTransaction={setTransaction} + selectedTransaction={transaction} /> ) : null} diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx index 66e1067..fe46f68 100644 --- a/app/client/src/components/TransactionList.tsx +++ b/app/client/src/components/TransactionList.tsx @@ -34,7 +34,7 @@ export default function TransactionList({ session }: listProp) { .catch(() => { //alert("Failed to get accounts"); }); - }, [session]); + }, [session, selectedAccount]); //Try to get the account's transaction from the server useEffect(() => { @@ -50,7 +50,7 @@ export default function TransactionList({ session }: listProp) { alert("Failed to get transaction"); }); } - }); + }, [session, selectedAccount]); //Adds a account to the list const addTransaction = (newTransaction: Transaction) => { @@ -106,7 +106,7 @@ export default function TransactionList({ session }: listProp) { ))} diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index 0b7ccde..e65d5ae 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -35,8 +35,8 @@ export default function PopupForm({ const handleSubmit = () => { const transferAmount = Number(amount); let userTransaction: Transaction; - let to = ""; - let from = ""; + let recipient = ""; + let sender = ""; if ( selectedType && @@ -56,18 +56,18 @@ export default function PopupForm({ (account) => account.id === selectedAccount, )?.name; if (target) { - to = target; + recipient = target; } - from = other; + sender = other; } else { - to = other; + recipient = other; target = account.find( (account) => account.id === selectedAccount, )?.name; if (target) { - from = target; + sender = target; } } } @@ -75,8 +75,8 @@ export default function PopupForm({ userTransaction = { id: 0, financialAccount_id: Number(selectedAccount), - to: to, - from: from, + recipient: recipient, + sender: sender, amount: transferAmount, category: selectedType, date: new Date(selectedDate), @@ -100,16 +100,14 @@ export default function PopupForm({ } else { try { //Send a request to create the transaction - postTranscations(session, [userTransaction]).then((response) => { + postTranscations(session, userTransaction).then((response) => { //Successful put if response is returned - if (response && response[0].id) { - userTransaction.id = response[0].id; + if (response && response.id) { + userTransaction.id = response.id; if (addTransaction) { addTransaction(userTransaction); } - } else { - alert("Failed to create transaction"); } }); } catch { @@ -163,7 +161,7 @@ export default function PopupForm({ //Holds state of user input const [selectedAccount, setSelectedAccount] = useState(() => { if (edit && selectedTransaction) { - return selectedTransaction.id; + return selectedTransaction.financialAccount_id; } else { return 0; } @@ -188,14 +186,25 @@ export default function PopupForm({ const [selectedDate, setSelectedDate] = useState(() => { if (edit && selectedTransaction) { - return selectedTransaction.date.toISOString(); + return new Date(selectedTransaction.date).toISOString().slice(0, 10); } else { //Default return ""; } }); - const [other, setOther] = useState(""); + const [other, setOther] = useState(() => { + if (edit && selectedTransaction) { + if (selectedType === transactionCategory.INCOME) { + return selectedTransaction.sender; + } else { + return selectedTransaction.recipient; + } + } else { + //Default + return ""; + } + }); //Holds the types of transfers const transCat: typeOfTransaction[] = Object.keys( @@ -227,13 +236,14 @@ export default function PopupForm({

{selectedType == transactionCategory.INCOME ? ( - + ) : ( - + )} setOther(event.target.value)} >

diff --git a/app/client/src/types/Transaction.tsx b/app/client/src/types/Transaction.tsx index b54eb5e..1620df8 100644 --- a/app/client/src/types/Transaction.tsx +++ b/app/client/src/types/Transaction.tsx @@ -4,7 +4,7 @@ export type Transaction = { amount: number; category: string; date: Date; // ISO format date string - from: string; - to: string; + sender: string; + recipient: string; description?: string; }; diff --git a/app/server/services/ts/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts index 971abb8..8b42f80 100644 --- a/app/server/services/ts/user/src/routes/account.ts +++ b/app/server/services/ts/user/src/routes/account.ts @@ -6,12 +6,11 @@ import type { Request, Response } from "express"; import type { ResultSetHeader } from "mysql2"; import { getConnectionPool } from "@/sqlUtil"; import type { financialAccount } from "@/types.js"; -import jwt from "jsonwebtoken"; +import { authenticateJWT } from "../handleJWT.js"; //import { authenticateJWT } from "../handleJWT.js"; //import { error } from "node:console"; -const JWT_SECRET = process.env.JWT_SECRET; export const accountsRouter = Router(); const db = getConnectionPool(); @@ -181,30 +180,6 @@ accountsRouter.delete("/", async (req: Request, res: Response) => { } }); -export const authenticateJWT = (req: Request) => { - const authHeader = req.headers.authorization; - //console.log(req.headers); - if (!authHeader) { - throw new Error("Authorization header missing"); - } - - const parts = authHeader.split(" "); - if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { - throw new Error("Invalid authorization header format"); - } - const token = parts[1]; - - //Add as any if it doesn't work - const decoded = jwt.verify(token, JWT_SECRET); - const userId = decoded.sub; - //console.log("user id: " + userId); - if (!userId) { - throw new Error("User ID not found in token"); - } - - return userId; -}; - //Checks the user is the owner of the account async function checkUserId( userId: number, diff --git a/app/server/services/ts/user/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts index c7c0444..c53cdb6 100644 --- a/app/server/services/ts/user/src/routes/transaction.ts +++ b/app/server/services/ts/user/src/routes/transaction.ts @@ -3,34 +3,57 @@ import { Router } from "express"; import type { Request, Response } from "express"; import type { ResultSetHeader } from "mysql2"; import { getConnectionPool } from "@/sqlUtil"; +import { authenticateJWT } from "../handleJWT.js"; +import type { Transaction } from "@/types.js"; export const transactionsRouter = Router(); const db = getConnectionPool(); // Get all transactions for a specific financial account transactionsRouter.get("/", async (req: Request, res: Response) => { + let userId; + try { - const { account_id } = req.query; + const financialAccount_id = Number(req.query.financialAccount_id); - if (!account_id) { - return res.status(400).json({ error: "Missing account_id" }); + if (!financialAccount_id) { + return res.status(400).json({ error: "Missing financialAccount_id" }); } - const [rows] = await db.query( + try { + userId = authenticateJWT(req); + } catch (error) { + console.error(error); + return res + .status(401) + .json({ error: "User not authorized to create a transaction" }); + } + + if (!(await checkUserId(userId, financialAccount_id))) { + console.error( + "User is not authorized to create a transaction on this account", + ); + return res.status(401).json({ + error: "User not authorized to create transaction on this account", + }); + } + + const [rows] = await db.query( `SELECT * FROM transaction WHERE financialAccount_id = ?`, - [account_id], + [financialAccount_id], ); - res.json(rows); + res.status(200).json(rows); } catch (err) { console.error("Failed to fetch transactions", err); - res.status(500).json({ error: "Failed to fetch transactions" }); + return res.status(500).json({ error: "Failed to fetch transactions" }); } }); //creating a new transaction transactionsRouter.post("/", async (req: Request, res: Response) => { + let userId; try { const { financialAccount_id, @@ -42,6 +65,26 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { category, } = req.body; + const sqlDate = convertToDateTime(date); + + try { + userId = authenticateJWT(req); + } catch (error) { + console.error(error); + return res + .status(401) + .json({ error: "User not authorized to create a transaction" }); + } + + if (!(await checkUserId(userId, financialAccount_id))) { + console.error( + "User is not authorized to create a transaction on this account", + ); + return res.status(401).json({ + error: "User not authorized to create transaction on this account", + }); + } + const [result] = await db.query( `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date, category ) VALUES (?, ?, ?, ?, ?, ?, ?)`, @@ -51,12 +94,12 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { description ?? null, sender ?? null, recipient ?? null, - date, + sqlDate, category ?? null, ], ); - res.json({ + res.status(200).json({ message: "Transaction successfully created", id: result.insertId, }); @@ -67,10 +110,39 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { }); // Update an existing transaction -transactionsRouter.put("/:id", async (req: Request, res: Response) => { +transactionsRouter.put("/", async (req: Request, res: Response) => { + let userId; + try { - const id = req.params.id; - const { amount, description, sender, recipient, date } = req.body; + const { + id, + financialAccount_id, + amount, + description, + sender, + recipient, + date, + } = req.body; + + const sqlDate = convertToDateTime(date); + + try { + userId = authenticateJWT(req); + } catch (error) { + console.error(error); + return res + .status(401) + .json({ error: "User not authorized to create a transaction" }); + } + + if (!(await checkUserId(userId, financialAccount_id))) { + console.error( + "User is not authorized to create a transaction on this account", + ); + return res.status(401).json({ + error: "User not authorized to create transaction on this account", + }); + } await db.query( `UPDATE transaction @@ -81,7 +153,7 @@ transactionsRouter.put("/:id", async (req: Request, res: Response) => { description ?? null, sender ?? null, recipient ?? null, - date, + sqlDate, id, ], ); @@ -93,13 +165,35 @@ transactionsRouter.put("/:id", async (req: Request, res: Response) => { }); // DELETE /transactions/:id -transactionsRouter.delete("/:id", async (req: Request, res: Response) => { +transactionsRouter.delete("/", async (req: Request, res: Response) => { try { - const id = req.params.id; + const { id, financialAccount_id } = req.body; + let userId; + + if (!(id && financialAccount_id)) { + console.error("Failed to delete transaction, parameter not recieved"); + return res.status(400).json({ error: "Bad request" }); + } + + try { + userId = authenticateJWT(req); + } catch (error) { + console.error(error); + return res + .status(401) + .json({ error: "User not authorized to delete transaction" }); + } + + if (!(await checkUserId(userId, financialAccount_id))) { + console.error("User is not authorized to delete this transaction"); + return res + .status(401) + .json({ error: "User not authorized to delete this transaction" }); + } await db.query(`DELETE FROM transaction WHERE id=?`, [id]); - res.json({ message: "Transaction deleted" }); + res.status(200).json({ message: "Transaction deleted" }); } catch (err) { console.error("Transaction deletion failed", err); res.status(500).json({ error: "Transaction deletion failed" }); @@ -183,3 +277,32 @@ transactionsRouter.post( } }, ); + +//Checks the user is the owner of the account +async function checkUserId( + userId: number, + accountId: number, +): Promise { + let result = false; + try { + //Checks if the profile has an account with that id + const [rows] = await db.query( + `SELECT 1 FROM profile_financialAccount + WHERE profile_id =? AND financialAccount_id =?`, + [userId, accountId], + ); + + console.log(rows.length); + if (rows.length > 0) { + result = true; + } + } catch (error) { + console.error(error); + } + + return result; +} + +function convertToDateTime(date: string) { + return date.slice(0, 19).replace("T", " "); +} From 5379e8dddf3a54a4d62e07afc291c373eace2ef2 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 18:57:24 -0600 Subject: [PATCH 178/204] Updated csv route for transactions --- .../ts/user/src/routes/transaction.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/app/server/services/ts/user/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts index c53cdb6..84e9363 100644 --- a/app/server/services/ts/user/src/routes/transaction.ts +++ b/app/server/services/ts/user/src/routes/transaction.ts @@ -204,6 +204,8 @@ transactionsRouter.delete("/", async (req: Request, res: Response) => { transactionsRouter.post( "/csvTransaction", async (req: Request, res: Response) => { + let userId; + try { const { financialAccount_id, transactions } = req.body; @@ -211,7 +213,25 @@ transactionsRouter.post( return res.status(400).json({ error: "Invalid request payload" }); } - // expected shape of a validated CSV row + // Authenticate user + try { + userId = authenticateJWT(req); + } catch (error) { + console.error(error); + return res + .status(401) + .json({ error: "User not authorized to import transactions" }); + } + + // Check account ownership + if (!(await checkUserId(userId, financialAccount_id))) { + console.error("User is not authorized to import into this account"); + return res.status(401).json({ + error: "User not authorized to import into this account", + }); + } + + // CSV row shape interface CsvRow { amount: number | null; description: string | null; @@ -222,6 +242,7 @@ transactionsRouter.post( errors: string[]; } + // Filter valid rows const validRows = transactions.filter( (t: unknown): t is CsvRow => typeof t === "object" && @@ -242,8 +263,11 @@ transactionsRouter.post( await connection.beginTransaction(); for (const row of validRows) { + const sqlDate = row.date ? convertToDateTime(row.date) : null; + await connection.query( - `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date, category) + `INSERT INTO transaction + (financialAccount_id, amount, description, sender, recipient, \`date\`, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ financialAccount_id, @@ -251,7 +275,7 @@ transactionsRouter.post( row.description ?? null, row.sender ?? null, row.recipient ?? null, - row.date, + sqlDate, row.category ?? "Uncategorized", ], ); @@ -277,7 +301,6 @@ transactionsRouter.post( } }, ); - //Checks the user is the owner of the account async function checkUserId( userId: number, From c9eca726b1e50547b23d4d168a62ddc323c32a95 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 19:54:05 -0600 Subject: [PATCH 179/204] updated csv transaction route --- app/client/src/api/Transaction.ts | 32 +++++++------- app/client/src/components/TransationForm.tsx | 10 ++++- .../src/components/csvread/CsvUpload.tsx | 42 ++++++++++++------- .../ts/user/src/routes/transaction.ts | 33 +++++++++++++-- 4 files changed, 81 insertions(+), 36 deletions(-) diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index a6a12ae..5ff2348 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,6 +1,7 @@ import type { AuthSession } from "@/pages/authTypes"; import type { updateResponse } from "../types/responseTypes"; import type { Transaction } from "../types/Transaction"; +import type { TransactionDraft } from "@/utils/ConvertTransaction"; const requestUrl = "http://localhost:3000/api/transactions"; @@ -119,21 +120,20 @@ export async function deleteTransaction( export async function uploadCsvTransactions( session: AuthSession, financialAccount_id: number, - transactions: Transaction[], -) { - try { - const response = await fetch(`${requestUrl}/csvTransaction`, { - method: "POST", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify({ financialAccount_id, transactions }), - }); + transactions: TransactionDraft[], +): Promise<{ + inserted: number; + skipped: number; + transactions: Transaction[]; +}> { + const response = await fetch(`${requestUrl}/csvTransaction`, { + method: "POST", + headers: { + "content-type": "application/json", + Authorization: `Bearer ${session.token}`, + }, + body: JSON.stringify({ financialAccount_id, transactions }), + }); - return response.json(); - } catch (error) { - console.error(error); - throw error; - } + return response.json(); } diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index e65d5ae..506d450 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -293,7 +293,15 @@ export default function PopupForm({ <> {!edit && selectedAccount && ( - + { + if (addTransaction) { + newTxs.forEach((tx) => addTransaction(tx)); + } + }} + /> )} )} diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index 7700dc3..ad3d4b6 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -9,8 +9,19 @@ import CsvConfirmation from "./CsvConfirm"; import { parseCsvFile } from "../../utils/ParseCsv"; import type { TransactionDraft } from "../../utils/ConvertTransaction"; import SuccessScreen from "./csvSuccess"; - -export default function CsvUpload({ accountId }: { accountId: number }) { +import { uploadCsvTransactions } from "@/api/Transaction"; +import type { AuthSession } from "@/pages/authTypes"; +import type { Transaction } from "@/types/Transaction"; + +export default function CsvUpload({ + accountId, + session, + onImported, +}: { + accountId: number; + session: AuthSession; + onImported: (txs: Transaction[]) => void; +}) { const [file, setFile] = useState(null); // selected CSV file const [error, setError] = useState(null); // validation errors const [parsedData, setParsedData] = useState(null); // state for parsed and validated data @@ -92,25 +103,24 @@ export default function CsvUpload({ accountId }: { accountId: number }) { if (!parsedData) return; try { - const response = await fetch("/api/transactions/csvTransaction", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - financialAccount_id: accountId, - transactions: parsedData, - }), - }); - - const result = await response.json(); + const response = await uploadCsvTransactions( + session, + accountId, + parsedData, + ); - if (!response.ok) { - setError(result.error || "Import failed"); + if ("error" in response) { + const err = response.error; + setError(typeof err === "string" ? err : "Unknown error"); return; } + // Notify parent (PopupForm) + onImported(response.transactions); + setImportResult({ - inserted: result.inserted, - skipped: result.skipped, + inserted: response.inserted, + skipped: response.skipped, }); } catch (error) { console.error("Import failed:", error); diff --git a/app/server/services/ts/user/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts index 84e9363..05bc8a3 100644 --- a/app/server/services/ts/user/src/routes/transaction.ts +++ b/app/server/services/ts/user/src/routes/transaction.ts @@ -242,6 +242,18 @@ transactionsRouter.post( errors: string[]; } + // Returned transaction shape + interface CreatedTransaction { + id: number; + financialAccount_id: number; + amount: number; + category: string; + date: Date; + sender: string; + recipient: string; + description: string; + } + // Filter valid rows const validRows = transactions.filter( (t: unknown): t is CsvRow => @@ -262,10 +274,12 @@ transactionsRouter.post( try { await connection.beginTransaction(); + const created: CreatedTransaction[] = []; + for (const row of validRows) { const sqlDate = row.date ? convertToDateTime(row.date) : null; - await connection.query( + const [result] = await connection.query( `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, \`date\`, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, @@ -279,14 +293,26 @@ transactionsRouter.post( row.category ?? "Uncategorized", ], ); + + created.push({ + id: result.insertId, + financialAccount_id, + amount: row.amount ?? 0, + category: row.category ?? "Uncategorized", + date: new Date(sqlDate ?? ""), + sender: row.sender ?? "", + recipient: row.recipient ?? "", + description: row.description ?? "", + }); } await connection.commit(); res.json({ message: "CSV transactions imported successfully", - inserted: validRows.length, - skipped: transactions.length - validRows.length, + inserted: created.length, + skipped: transactions.length - created.length, + transactions: created, }); } catch (err) { await connection.rollback(); @@ -301,6 +327,7 @@ transactionsRouter.post( } }, ); + //Checks the user is the owner of the account async function checkUserId( userId: number, From 41bd69453cfe75973a4cda9a2b64b620ddd1bf5c Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 20:00:09 -0600 Subject: [PATCH 180/204] sample csv file used for testing --- app/client/test/sampleCSV1.csv | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/client/test/sampleCSV1.csv diff --git a/app/client/test/sampleCSV1.csv b/app/client/test/sampleCSV1.csv new file mode 100644 index 0000000..df5dc67 --- /dev/null +++ b/app/client/test/sampleCSV1.csv @@ -0,0 +1,11 @@ +date,description,amount,sender,recipient,category +3/1/2024,Coffee Shop,25.5,Me,Starbucks,Food +03/01/2024,Rent Payment,1000,Me,Landlord,Housing +2024-03-03,Grocery Run,45.2,Me,Superstore,Food +3/4/2024,Internet Bill,95,Me,Bell MTS,Utilities +3/5/2024,Gas,60,Me,Shell,Transport +3/6/2024,Restaurant,75,Me,Earls,Food +3/7/2024,Phone Bill,70,Me,Rogers,Utilities +3/8/2024,Gift Received,150,Aunt,,Gifts +3/9/2024,Freelance Payment,600,Client,,Income +3/10/2024,Gym Membership,45,Me,Gym,Fitness From 368ff8b01f67a73c60f596c9a9f6d9d8aa2db104 Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 21:10:50 -0600 Subject: [PATCH 181/204] Fixed import for auth session --- app/client/src/components/csvread/CsvUpload.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index ad3d4b6..3fe42d1 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -10,7 +10,7 @@ import { parseCsvFile } from "../../utils/ParseCsv"; import type { TransactionDraft } from "../../utils/ConvertTransaction"; import SuccessScreen from "./csvSuccess"; import { uploadCsvTransactions } from "@/api/Transaction"; -import type { AuthSession } from "@/pages/authTypes"; +import type { AuthSession } from "@/types/authTypes"; import type { Transaction } from "@/types/Transaction"; export default function CsvUpload({ From 0f17484bfdcfc0eb95abc5c0646a16ad8dd0620d Mon Sep 17 00:00:00 2001 From: Dylan Prabagaran Date: Fri, 6 Mar 2026 21:27:43 -0600 Subject: [PATCH 182/204] Fixed a test to allign with new normalizeRow --- app/client/test/normalizeRow.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/client/test/normalizeRow.test.ts b/app/client/test/normalizeRow.test.ts index 33bdb71..925a097 100644 --- a/app/client/test/normalizeRow.test.ts +++ b/app/client/test/normalizeRow.test.ts @@ -51,12 +51,6 @@ describe("normalizeRow", () => { const result = normalizeRow(raw); expect(result.date).toBe("2024-01-31"); }); - //tests for DD-MM-YYYY date format - it("normalizes DD-MM-YYYY date format", () => { - const raw = { date: "31-01-2024" }; - const result = normalizeRow(raw); - expect(result.date).toBe("2024-01-31"); - }); //tests for invalid date formats it("returns null for invalid dates", () => { const raw = { date: "not-a-date" }; From 3c18838d79e1029fe6d122463f2500a603e81134 Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Fri, 6 Mar 2026 21:37:48 -0600 Subject: [PATCH 183/204] Updated and Complete Test Plan for Sprint 2 --- .../{Finus_Test_Plan.docx.md => Finus_Test_Plan.md} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename documentation/{Finus_Test_Plan.docx.md => Finus_Test_Plan.md} (94%) diff --git a/documentation/Finus_Test_Plan.docx.md b/documentation/Finus_Test_Plan.md similarity index 94% rename from documentation/Finus_Test_Plan.docx.md rename to documentation/Finus_Test_Plan.md index d8ba632..82fea07 100644 --- a/documentation/Finus_Test_Plan.docx.md +++ b/documentation/Finus_Test_Plan.md @@ -31,6 +31,11 @@ These are the features we have implemented for Sprint 2 and have created tests f 3\. User Data Input +1. CSV parsing logic +2. Data input +3. Validating forms and files + + The following are the features to be implemented in future sprints: Financial Goals, Financial Projections, Stock and FOREX Tracking, Collaborative Budgets (stretch), and ML Integration (stretch). The scope includes functional verification through unit, integration, acceptance, and regression testing for the features implemented. @@ -108,7 +113,7 @@ Test levels define the Types of Testing to be executed on the Application Under | Test Level | Scope & Requirement | Methodology (How will you do this?) | | :---- | :---- | :---- | -| **Unit Testing** | **User Authentication:** 20 tests **User Input: Visualization Dashboard:** 50 in Python, 46 in TypeScript **Total:** | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | +| **Unit Testing** | **User Authentication:** 20 tests \n **User Input:** 30 tests \n Visualization Dashboard:** 50 in Python, 46 in TypeScript \n **Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | | **Integration Testing** | **10 tests total** covering interactions between features. | *Running tests in their environment with microservices fully operational and communicating with each other.* | | **Acceptance Testing** | **End-user testing** for every user story. | *Team members/external users will perform Manual Walkthroughs based on User Story criteria. Deficiencies and potential improvements will be documented.* | | **Regression Testing** | Unit \+ Integration tests executed on **every push to main branch**. | *We have configured a GitHub Actions CI pipeline to run all tests automatically.* | From a58fb63ae9e7d9f55f257e83e958515244f8bb3f Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Fri, 6 Mar 2026 21:41:37 -0600 Subject: [PATCH 184/204] Fixed minor markdown syntax in test plan --- documentation/Finus_Test_Plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/Finus_Test_Plan.md b/documentation/Finus_Test_Plan.md index 82fea07..ef27741 100644 --- a/documentation/Finus_Test_Plan.md +++ b/documentation/Finus_Test_Plan.md @@ -113,7 +113,7 @@ Test levels define the Types of Testing to be executed on the Application Under | Test Level | Scope & Requirement | Methodology (How will you do this?) | | :---- | :---- | :---- | -| **Unit Testing** | **User Authentication:** 20 tests \n **User Input:** 30 tests \n Visualization Dashboard:** 50 in Python, 46 in TypeScript \n **Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | +| **Unit Testing** | **User Authentication:** 20 tests
**User Input:** 30 tests
Visualization Dashboard:** 50 in Python, 46 in TypeScript
**Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | | **Integration Testing** | **10 tests total** covering interactions between features. | *Running tests in their environment with microservices fully operational and communicating with each other.* | | **Acceptance Testing** | **End-user testing** for every user story. | *Team members/external users will perform Manual Walkthroughs based on User Story criteria. Deficiencies and potential improvements will be documented.* | | **Regression Testing** | Unit \+ Integration tests executed on **every push to main branch**. | *We have configured a GitHub Actions CI pipeline to run all tests automatically.* | From 6772cbe8a616cc29cd3d15380fb89eb9a4a8c11d Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Fri, 6 Mar 2026 21:42:41 -0600 Subject: [PATCH 185/204] One more minor markdown syntax fix in test plan --- documentation/Finus_Test_Plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/Finus_Test_Plan.md b/documentation/Finus_Test_Plan.md index ef27741..bff544c 100644 --- a/documentation/Finus_Test_Plan.md +++ b/documentation/Finus_Test_Plan.md @@ -113,7 +113,7 @@ Test levels define the Types of Testing to be executed on the Application Under | Test Level | Scope & Requirement | Methodology (How will you do this?) | | :---- | :---- | :---- | -| **Unit Testing** | **User Authentication:** 20 tests
**User Input:** 30 tests
Visualization Dashboard:** 50 in Python, 46 in TypeScript
**Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | +| **Unit Testing** | **User Authentication:** 20 tests
**User Input:** 30 tests
**Visualization Dashboard:** 50 in Python, 46 in TypeScript
**Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | | **Integration Testing** | **10 tests total** covering interactions between features. | *Running tests in their environment with microservices fully operational and communicating with each other.* | | **Acceptance Testing** | **End-user testing** for every user story. | *Team members/external users will perform Manual Walkthroughs based on User Story criteria. Deficiencies and potential improvements will be documented.* | | **Regression Testing** | Unit \+ Integration tests executed on **every push to main branch**. | *We have configured a GitHub Actions CI pipeline to run all tests automatically.* | From 769784edb2f7d361eb9c537c69ac30724a75f878 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:14:11 -0500 Subject: [PATCH 186/204] created dev and prod terraform modules (#86) * created dev and prod terraform modules * rename VPC from Finus2 to Finus --- deployment/.gitignore | 44 ++++++++++++++++++ deployment/.terraform.lock.hcl | 25 ++++++++++ deployment/main.tf | 79 ++++++++++++++++++++++++++++++++ deployment/modules/finus/main.tf | 77 +++++++++++++++++++++++++++++++ deployment/terraform.tf | 9 ++++ 5 files changed, 234 insertions(+) create mode 100644 deployment/.gitignore create mode 100644 deployment/.terraform.lock.hcl create mode 100644 deployment/main.tf create mode 100644 deployment/modules/finus/main.tf create mode 100644 deployment/terraform.tf diff --git a/deployment/.gitignore b/deployment/.gitignore new file mode 100644 index 0000000..66ee655 --- /dev/null +++ b/deployment/.gitignore @@ -0,0 +1,44 @@ +# Local .terraform directories +.terraform/ + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Optional: ignore graph output files generated by `terraform graph` +# *.dot + +# Optional: ignore plan files saved before destroying Terraform configuration +# Uncomment the line below if you want to ignore planout files. +# planout diff --git a/deployment/.terraform.lock.hcl b/deployment/.terraform.lock.hcl new file mode 100644 index 0000000..3bfa040 --- /dev/null +++ b/deployment/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.37.0" + constraints = "~> 6.37.0" + hashes = [ + "h1:w3z/TApcKD3b/aMZoZZKSxOld4xw+gEtQ1ka6C1UN+4=", + "zh:0427fadb719ed5a32feb09f047539d2348e659056f3b8a8589d34d8f0a95be7a", + "zh:3891c670674aba2125a7ac6d4348cde43646b1b46ce6f829e6f4724091bc0dcd", + "zh:632cb24b7b5790b730b33bcbe9f1a7b75f2644fb52f9d6aaafb0249c9e7601d2", + "zh:6e96ed1f824c2efa9de5b7c22ab3715624ba34c28564a06e9a15e71bc3d3a30b", + "zh:7b8fd86907b659bc45f4a3f42c3c0ccc66925a74e265b01e9e66242c0b2cafef", + "zh:81f9a587deddef4dfcc2101c54ec28a3a554056837f68ebb920c83fe8327b16f", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a9a38a67cb98d690fec951ec3e133b6836279629db2ed3a0ebf97a5bea58674f", + "zh:b18f60d62e4bd4d466077e09c39259d1a85355f0f00b801fe8aedbc50193d357", + "zh:b7a51bc0faf60d17043b4df1d1b7bb55129eaa4bdeb65ff55f5b00b9b8fee9f7", + "zh:c28c42f91ca3a6b65b3fd3ed6e891fc0fc28d0cb5ab65dea65eda8eec5cea5f3", + "zh:d895ddc04280ed26b6ca64ca05b78caaa7b72c8e167af4093545efbc608d5482", + "zh:f4a56f5157009ef160fbd79105078fe675df479cb73c1b7e1fea2741403a0b67", + "zh:f547d6ca371b96fec97b972fc0c93bcfc23d58e34a9da215b94e9d2aa170fb77", + "zh:f7b0a3cd4adadd3f4b9609a54e651ed5eafa22c196ab229042fc1d0aa0ab8f3a", + ] +} diff --git a/deployment/main.tf b/deployment/main.tf new file mode 100644 index 0000000..88f8ce6 --- /dev/null +++ b/deployment/main.tf @@ -0,0 +1,79 @@ +provider "aws" { + region = "us-east-2" +} + +resource "aws_vpc" "finus" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "Finus" + } +} + +resource "aws_subnet" "prod" { + vpc_id = aws_vpc.finus.id + cidr_block = "10.0.0.0/17" +} + +resource "aws_subnet" "dev" { + vpc_id = aws_vpc.finus.id + cidr_block = "10.0.128.0/17" +} + +resource "aws_internet_gateway" "default" { + vpc_id = aws_vpc.finus.id +} + +resource "aws_security_group" "default" { + name = "finus-default" + vpc_id = aws_vpc.finus.id + + ingress { + description = "SSH from anywhere" + cidr_blocks = ["0.0.0.0/0"] + from_port = "22" + to_port = "22" + protocol = "tcp" + } + + egress { + description = "HTTP from anywhere" + from_port = "3000" + to_port = "3000" + protocol = "tcp" + } +} + +resource "aws_route_table" "route_table" { + vpc_id = aws_vpc.finus.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.default.id + } +} + +module "prod" { + source = "./modules/finus" + + environment_name = "prod" + ssh_key_pair_name = "finus-dev" + security_group_ids = [aws_security_group.default.id] + subnet_id = aws_subnet.prod.id + vpc_id = aws_vpc.finus.id + gateway_id = aws_internet_gateway.default.id + route_table_id = aws_route_table.route_table.id +} + + +module "dev" { + source = "./modules/finus" + + environment_name = "dev" + ssh_key_pair_name = "finus-dev" + security_group_ids = [aws_security_group.default.id] + subnet_id = aws_subnet.dev.id + vpc_id = aws_vpc.finus.id + gateway_id = aws_internet_gateway.default.id + route_table_id = aws_route_table.route_table.id +} diff --git a/deployment/modules/finus/main.tf b/deployment/modules/finus/main.tf new file mode 100644 index 0000000..cf683eb --- /dev/null +++ b/deployment/modules/finus/main.tf @@ -0,0 +1,77 @@ +variable "environment_name" { + type = string +} +variable "ssh_key_pair_name" { + type = string +} +variable "security_group_ids" { + type = list(string) +} +variable "subnet_id" { + type = string +} +variable "vpc_id" { + type = string +} +variable "gateway_id" { + type = string +} +variable "route_table_id" { + type = string +} + + +data "aws_ami" "amazon_linux" { + filter { + name = "image-id" + values = ["ami-0b0b78dcacbab728f"] + } +} + + +resource "aws_instance" "backend" { + instance_type = "t2.nano" + ami = data.aws_ami.amazon_linux.id + subnet_id = var.subnet_id + key_name = var.ssh_key_pair_name + vpc_security_group_ids = var.security_group_ids + + tags = { + Name = join("", ["finus-", var.environment_name, "-backend"]) + } +} + +resource "aws_instance" "webserver" { + instance_type = "t2.nano" + ami = data.aws_ami.amazon_linux.id + subnet_id = var.subnet_id + key_name = var.ssh_key_pair_name + vpc_security_group_ids = var.security_group_ids + + tags = { + Name = join("", ["finus-", var.environment_name, "-webserver"]) + } +} + +resource "aws_route_table_association" "assoc" { + subnet_id = var.subnet_id + route_table_id = var.route_table_id +} + +resource "aws_eip" "backend" { + instance = aws_instance.backend.id + domain = "vpc" + + tags = { + Name = join("", ["finus-", var.environment_name, "-backend"]) + } +} + +resource "aws_eip" "webserver" { + instance = aws_instance.webserver.id + domain = "vpc" + + tags = { + Name = join("", ["finus-", var.environment_name, "-webserver"]) + } +} diff --git a/deployment/terraform.tf b/deployment/terraform.tf new file mode 100644 index 0000000..aaa5ff0 --- /dev/null +++ b/deployment/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.37.0" + } + } + required_version = ">= 1.2" +} From 640a39ec6f14e6cbc05fca6a58020a146746a235 Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Fri, 27 Mar 2026 11:24:16 -0500 Subject: [PATCH 187/204] Squashed feature stock tracking - merge into dev (#91) * Initialized models for the api fetchers * Basic init stuff * Added market service for stock and forex search, as well as retrieving historical data using Yahoo Finance API * Search bar for markets (WIP) * Implementing the historical Chart visualization chart * created an enum of predined markets to choose from * Added Market Page to Finus app * Added frontend logic for the stock and forex market dashboard * Added Search Bar and Pinned Instruments feature * Initalized unit tests for Market * Deleted redundand file * Integration Tests for user input (WIP) * fix bug or something * Bug fixed for integration tests User input * Removed Debugging Console logs * fixed issue where wrong address was being used + integration tests were running before database ready for connections * Deleted outdated files * Integrated the historical chart section * Made the pinned instruments section look more clean * Fixed bug relating to persistence of pinned instruments * Added unit tests for market with 100% coverage * Sequence diagram * Added integration and mutation tests for market * pushing MarketAcceptance doc * Changed the docker compose to run test workflow in CI * Fixed some tests so now they pass * Updated CI workflow * User accpetance test * UserInput client docker * get docker compose in order file client * get docker-compose.test.yml to build base webserver image * fixed it? * Mutation Testing stuff --------- Co-authored-by: Dylan Prabagaran Co-authored-by: Logan --- .github/workflows/run_tests.yml | 33 +- app/client/Dockerfile | 17 +- app/client/Dockerfile.base | 15 + app/client/Dockerfile.mutation.test | 10 + app/client/Dockerfile.test | 8 + app/client/bun.lock | 250 +- app/client/bun.lockb | Bin 181938 -> 205522 bytes app/client/docker-compose.test.yml | 21 + app/client/docker-compose.yml | 10 +- app/client/package.json | 5 +- app/client/src/App.tsx | 37 +- app/client/src/api/Market.ts | 91 + .../src/components/MarketDashboardSection.tsx | 388 ++ app/client/src/components/NavBar.tsx | 6 +- .../src/components/market/MarketSearchBar.tsx | 34 + .../market/MarketSearchResultsPanel.tsx | 147 + .../market/PinnedInstrumentsSection.tsx | 114 + .../market/SelectedInstrumentPanel.tsx | 301 ++ .../components/market/marketSectionHelpers.ts | 48 + app/client/src/pages/MarketsPage.tsx | 57 + app/client/src/types/Market.ts | 33 + app/client/src/utils/NormalizeRow.ts | 3 +- app/client/src/utils/ValidateFile.ts | 4 +- app/client/src/utils/ValidateForms.ts | 2 +- app/client/src/utils/handleInput.ts | 2 +- app/client/src/utils/market.ts | 129 + app/client/src/utils/marketStorage.ts | 106 + app/client/stryker.conf.json | 24 + app/client/test/handleInput.test.ts | 45 + app/client/test/normalizeRow.test.ts | 92 + app/client/test/parseCsvFile.test.ts | 64 + app/client/test/validateFile.test.ts | 9 + app/client/test/validateForms.test.ts | 140 - app/client/vitest.config.ts | 10 + app/server/docker-compose.test.yml | 37 +- app/server/docker-compose.yml | 9 + .../services/python/analytics/Dockerfile | 9 - .../python/analytics/test/budgetPerfCalc.py | 152 + .../analytics/test/budget_integration.py | 38 + .../python/analytics/test/cat_classifier.py | 55 + .../python/analytics/test/dependencies.py | 76 + .../python/analytics/test/incomeflow.py | 131 + .../python/analytics/test/periodCalc.py | 146 + .../services/python/analytics/test/savings.py | 99 + .../python/market/Dockerfile.integration.test | 13 + .../python/market/Dockerfile.mutation.test | 21 + .../services/python/market/Dockerfile.test | 15 + .../integration/test_market_integration.py | 181 + .../services/python/market/pyproject.toml | 8 + .../python/market/requirements-test.txt | 5 + .../services/python/market/src/__init__.py | 1 + .../services/python/market/src/config.py | 124 + app/server/services/python/market/src/main.py | 68 +- .../services/python/market/src/service.py | 58 + .../services/python/market/src/utils.py | 46 + .../python/market/src/yahoo_client.py | 171 + .../services/python/market/test/__init__.py | 1 + .../python/market/test/test_config.py | 149 + .../services/python/market/test/test_main.py | 136 + .../python/market/test/test_service.py | 447 ++ .../services/python/market/test/test_utils.py | 117 + .../python/market/test/test_yahoo_client.py | 783 +++ .../services/ts/api-gateway/src/index.ts | 56 + app/server/services/ts/market/.gitignore | 34 + app/server/services/ts/market/Dockerfile | 21 + app/server/services/ts/market/Dockerfile.test | 11 + app/server/services/ts/market/README.md | 15 + app/server/services/ts/market/bun.lock | 179 + app/server/services/ts/market/index.ts | 1 + app/server/services/ts/market/package.json | 15 + .../services/ts/market/tests/market.test.ts | 141 + app/server/services/ts/market/tsconfig.json | 25 + .../ts/market/types/HistoricalPoint.ts | 5 + .../services/ts/market/types/MarketModel.ts | 9 + .../ts/user/Dockerfile.integration.test | 9 + app/server/services/ts/user/bun.lock | 49 +- .../ts/user/integration/accounts.test.ts | 112 + .../services/ts/user/integration/setup.ts | 86 + .../ts/user/integration/transaction.test.ts | 166 + app/server/services/ts/user/package-lock.json | 4554 +++++++++++++++++ app/server/services/ts/user/package.json | 4 +- app/server/services/ts/user/src/handleJWT.ts | 9 +- .../services/ts/user/src/routes/account.ts | 16 + .../ts/user/src/routes/transaction.ts | 6 +- .../MarketAcceptance/MarketAcceptance.md | 61 + .../Acceptance/UserInp/UserInpAcceptance.md | 59 + .../stockTracking/SeqDiagramStock.png | Bin 0 -> 44388 bytes 87 files changed, 10780 insertions(+), 214 deletions(-) create mode 100644 app/client/Dockerfile.base create mode 100644 app/client/Dockerfile.mutation.test create mode 100644 app/client/Dockerfile.test create mode 100644 app/client/docker-compose.test.yml create mode 100644 app/client/src/api/Market.ts create mode 100644 app/client/src/components/MarketDashboardSection.tsx create mode 100644 app/client/src/components/market/MarketSearchBar.tsx create mode 100644 app/client/src/components/market/MarketSearchResultsPanel.tsx create mode 100644 app/client/src/components/market/PinnedInstrumentsSection.tsx create mode 100644 app/client/src/components/market/SelectedInstrumentPanel.tsx create mode 100644 app/client/src/components/market/marketSectionHelpers.ts create mode 100644 app/client/src/pages/MarketsPage.tsx create mode 100644 app/client/src/types/Market.ts create mode 100644 app/client/src/utils/market.ts create mode 100644 app/client/src/utils/marketStorage.ts create mode 100644 app/client/stryker.conf.json delete mode 100644 app/client/test/validateForms.test.ts create mode 100644 app/client/vitest.config.ts create mode 100644 app/server/services/python/analytics/test/budgetPerfCalc.py create mode 100644 app/server/services/python/analytics/test/budget_integration.py create mode 100644 app/server/services/python/analytics/test/cat_classifier.py create mode 100644 app/server/services/python/analytics/test/dependencies.py create mode 100644 app/server/services/python/analytics/test/incomeflow.py create mode 100644 app/server/services/python/analytics/test/periodCalc.py create mode 100644 app/server/services/python/analytics/test/savings.py create mode 100644 app/server/services/python/market/Dockerfile.integration.test create mode 100644 app/server/services/python/market/Dockerfile.mutation.test create mode 100644 app/server/services/python/market/Dockerfile.test create mode 100644 app/server/services/python/market/integration/test_market_integration.py create mode 100644 app/server/services/python/market/pyproject.toml create mode 100644 app/server/services/python/market/requirements-test.txt create mode 100644 app/server/services/python/market/src/__init__.py create mode 100644 app/server/services/python/market/src/config.py create mode 100644 app/server/services/python/market/src/service.py create mode 100644 app/server/services/python/market/src/utils.py create mode 100644 app/server/services/python/market/src/yahoo_client.py create mode 100644 app/server/services/python/market/test/__init__.py create mode 100644 app/server/services/python/market/test/test_config.py create mode 100644 app/server/services/python/market/test/test_main.py create mode 100644 app/server/services/python/market/test/test_service.py create mode 100644 app/server/services/python/market/test/test_utils.py create mode 100644 app/server/services/python/market/test/test_yahoo_client.py create mode 100644 app/server/services/ts/market/.gitignore create mode 100644 app/server/services/ts/market/Dockerfile create mode 100644 app/server/services/ts/market/Dockerfile.test create mode 100644 app/server/services/ts/market/README.md create mode 100644 app/server/services/ts/market/bun.lock create mode 100644 app/server/services/ts/market/index.ts create mode 100644 app/server/services/ts/market/package.json create mode 100644 app/server/services/ts/market/tests/market.test.ts create mode 100644 app/server/services/ts/market/tsconfig.json create mode 100644 app/server/services/ts/market/types/HistoricalPoint.ts create mode 100644 app/server/services/ts/market/types/MarketModel.ts create mode 100644 app/server/services/ts/user/Dockerfile.integration.test create mode 100644 app/server/services/ts/user/integration/accounts.test.ts create mode 100644 app/server/services/ts/user/integration/setup.ts create mode 100644 app/server/services/ts/user/integration/transaction.test.ts create mode 100644 app/server/services/ts/user/package-lock.json create mode 100644 documentation/Acceptance/MarketAcceptance/MarketAcceptance.md create mode 100644 documentation/Acceptance/UserInp/UserInpAcceptance.md create mode 100644 documentation/sequence-diagrams/stockTracking/SeqDiagramStock.png diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 0407fde..192b8a0 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [ "*" ] push: - branches: [ "main", "development", "release/*" ] + branches: [ "main", "development", "release/*", "feature-stock-tracking" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -20,7 +20,32 @@ jobs: run: docker compose build - name: Build test images run: | - mv docker-compose.test.yml docker-compose.yml - docker compose build + docker compose -f docker-compose.yml -f docker-compose.test.yml build + - name: Start backend services + run: docker compose -f docker-compose.yml up -d database auth user analytics market api-gateway - name: Run tests - run: docker compose up --abort-on-container-failure + run: | + failed=() + for service in \ + auth-test \ + user-test \ + analytics-test \ + market-test \ + market-mutation-test \ + market-integration-test \ + user-integration-test + do + echo "::group::Running $service" + if ! docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm "$service"; then + failed+=("$service") + fi + echo "::endgroup::" + done + + if [ ${#failed[@]} -gt 0 ]; then + echo "Failed test services: ${failed[*]}" + exit 1 + fi + - name: Stop services + if: always() + run: docker compose -f docker-compose.yml -f docker-compose.test.yml down -v --remove-orphans diff --git a/app/client/Dockerfile b/app/client/Dockerfile index 3d460ed..ec7fcdd 100644 --- a/app/client/Dockerfile +++ b/app/client/Dockerfile @@ -1,19 +1,6 @@ -FROM oven/bun:1.3.9-slim AS builder - -WORKDIR /app - -COPY package.json bun.lockb . -RUN --mount=type=cache,target=/root/.bun/install/cache \ -bun install - -COPY tsconfig.* . -COPY vite.config.ts . -COPY index.* . -COPY public ./public -COPY src ./src - +FROM finus-webserver-base:local as base RUN bun run build FROM nginx:1.29.5-alpine3.23-perl -COPY --from=builder /app/dist /data/www +COPY --from=base /app/dist /data/www COPY nginx.conf /etc/nginx/nginx.conf diff --git a/app/client/Dockerfile.base b/app/client/Dockerfile.base new file mode 100644 index 0000000..a5d1461 --- /dev/null +++ b/app/client/Dockerfile.base @@ -0,0 +1,15 @@ +FROM oven/bun:1.3.9-slim + +WORKDIR /app + +COPY package.json bun.lockb . +RUN --mount=type=cache,target=/root/.bun/install/cache \ + bun install + +COPY tsconfig.* . +COPY vite.config.ts . +COPY index.* . +COPY public ./public +COPY src ./src + +ENTRYPOINT [ "/bin/bash", "-c", "exit" ] diff --git a/app/client/Dockerfile.mutation.test b/app/client/Dockerfile.mutation.test new file mode 100644 index 0000000..92de251 --- /dev/null +++ b/app/client/Dockerfile.mutation.test @@ -0,0 +1,10 @@ +FROM oven/bun:1 + +WORKDIR /app + +COPY bun.lockb package.json ./ +RUN bun add -g @stryker-mutator/core @stryker-mutator/typescript-checker + +COPY . . + +CMD ["bun","run", "mutate"] diff --git a/app/client/Dockerfile.test b/app/client/Dockerfile.test new file mode 100644 index 0000000..2eb18e8 --- /dev/null +++ b/app/client/Dockerfile.test @@ -0,0 +1,8 @@ +FROM finus-webserver-base:local + +COPY test ./test +COPY vitest.config.ts . + +ENV CI=true + +ENTRYPOINT ["bun", "run", "vitest", "--run"] diff --git a/app/client/bun.lock b/app/client/bun.lock index 06868ca..993dbe8 100644 --- a/app/client/bun.lock +++ b/app/client/bun.lock @@ -26,6 +26,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@stryker-mutator/core": "^9.6.0", + "@stryker-mutator/vitest-runner": "^9.6.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -61,14 +63,28 @@ "@babel/generator": ["@babel/generator@7.29.0", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ=="], + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -79,6 +95,24 @@ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="], + + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], + + "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -155,6 +189,38 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="], + + "@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="], + + "@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/editor": ["@inquirer/editor@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/external-editor": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA=="], + + "@inquirer/expand": ["@inquirer/expand@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@2.0.4", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.4", "", {}, "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ=="], + + "@inquirer/input": ["@inquirer/input@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ=="], + + "@inquirer/number": ["@inquirer/number@4.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA=="], + + "@inquirer/password": ["@inquirer/password@5.0.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.3.2", "", { "dependencies": { "@inquirer/checkbox": "^5.1.2", "@inquirer/confirm": "^6.0.10", "@inquirer/editor": "^5.0.10", "@inquirer/expand": "^5.0.10", "@inquirer/input": "^5.0.10", "@inquirer/number": "^4.0.10", "@inquirer/password": "^5.0.10", "@inquirer/rawlist": "^5.2.6", "@inquirer/search": "^4.1.6", "@inquirer/select": "^5.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.2.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w=="], + + "@inquirer/search": ["@inquirer/search@4.1.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ=="], + + "@inquirer/select": ["@inquirer/select@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA=="], + + "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -329,10 +395,24 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@stryker-mutator/api": ["@stryker-mutator/api@9.6.0", "", { "dependencies": { "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-kJEEwOVoWDXGEIXuM+9efT6LSJ7nyxnQQvjEoKg8GSZXbDUjfD0tqA0aBD06U1SzQLKCM7ffjgPffr154MHZKw=="], + + "@stryker-mutator/core": ["@stryker-mutator/core@9.6.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/instrumenter": "9.6.0", "@stryker-mutator/util": "9.6.0", "ajv": "~8.18.0", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.2.4", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.7.2", "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.2.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-oSbw01l6HXHt0iW9x5fQj7yHGGT8ZjCkXSkI7Bsu0juO7Q6vRMXk7XcvKpCBgRgzKXi1osg8+iIzj7acHuxepQ=="], + + "@stryker-mutator/instrumenter": ["@stryker-mutator/instrumenter@9.6.0", "", { "dependencies": { "@babel/core": "~7.29.0", "@babel/generator": "~7.29.0", "@babel/parser": "~7.29.0", "@babel/plugin-proposal-decorators": "~7.29.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.28.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "angular-html-parser": "~10.4.0", "semver": "~7.7.0", "tslib": "2.8.1", "weapon-regex": "~1.3.2" } }, "sha512-tWdRYfm9LF4Go7cNOos0xEIOEnN7ZOSj38rfXvGZS9IINlvYBrBCl2xcz/67v6l5A7xksMWWByZRIq2bgdnnUg=="], + + "@stryker-mutator/util": ["@stryker-mutator/util@9.6.0", "", {}, "sha512-gw7fJOFNHEj9inAEOodD9RrrMEMhZmWJ46Ww/kDJAXlSsBBmdwCzeomNLngmLTvgp14z7Tfq85DHYwvmNMdOxA=="], + + "@stryker-mutator/vitest-runner": ["@stryker-mutator/vitest-runner@9.6.0", "", { "dependencies": { "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "tslib": "~2.8.0" }, "peerDependencies": { "@stryker-mutator/core": "9.6.0", "vitest": ">=2.0.0" } }, "sha512-/zyELz5jTDAiH0Hr23G6KSnBFl9XV+vn0T0qUAk4sPqJoP5NVm9jjpgt9EBACS/VTkVqSvXqBid4jmESPx11Sg=="], + "@swc/core": ["@swc/core@1.15.11", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.11", "@swc/core-darwin-x64": "1.15.11", "@swc/core-linux-arm-gnueabihf": "1.15.11", "@swc/core-linux-arm64-gnu": "1.15.11", "@swc/core-linux-arm64-musl": "1.15.11", "@swc/core-linux-x64-gnu": "1.15.11", "@swc/core-linux-x64-musl": "1.15.11", "@swc/core-win32-arm64-msvc": "1.15.11", "@swc/core-win32-ia32-msvc": "1.15.11", "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg=="], @@ -487,7 +567,9 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -509,23 +591,27 @@ "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], @@ -533,6 +619,8 @@ "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -541,6 +629,8 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -585,16 +675,22 @@ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], @@ -639,6 +735,8 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -647,10 +745,20 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], @@ -677,6 +785,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], @@ -701,6 +811,10 @@ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], @@ -709,6 +823,8 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], @@ -719,6 +835,12 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], @@ -729,6 +851,8 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -739,7 +863,9 @@ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], @@ -777,6 +903,8 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -797,18 +925,34 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mutation-server-protocol": ["mutation-server-protocol@0.4.1", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g=="], + + "mutation-testing-elements": ["mutation-testing-elements@3.7.2", "", {}, "sha512-i7X2Q4X5eYon72W2QQ9HND7plVhQcqTnv+Xc3KeYslRZSJ4WYJoal8LFdbWm7dKWLNE0rYkCUrvboasWzF3MMA=="], + + "mutation-testing-metrics": ["mutation-testing-metrics@3.7.2", "", { "dependencies": { "mutation-testing-report-schema": "3.7.2" } }, "sha512-ichXZSC4FeJbcVHYOWzWUhNuTJGogc0WiQol8lqEBrBSp+ADl3fmcZMqrx0ogInEUiImn+A8JyTk6uh9vd25TQ=="], + + "mutation-testing-report-schema": ["mutation-testing-report-schema@3.7.2", "", {}, "sha512-fN5M61SDzIOeJyatMOhGPLDOFz5BQIjTNPjo4PcHIEUWrejO4i4B5PFuQ/2l43709hEsTxeiXX00H73WERKcDw=="], + + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -823,9 +967,11 @@ "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -845,10 +991,16 @@ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], @@ -881,6 +1033,8 @@ "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -889,9 +1043,13 @@ "rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -899,11 +1057,21 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], - "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -911,6 +1079,8 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], @@ -937,20 +1107,32 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="], + + "typed-rest-client": ["typed-rest-client@2.2.0", "", { "dependencies": { "des.js": "^1.1.0", "js-md4": "^0.3.2", "qs": "^6.14.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-/e2Rk9g20N0r44kaQLb3v6QGuryOD8SPb53t43Y5kqXXA+SqWuU7zLiMxetw61jNn/JFrxTdr5nPDhGY/eTNhQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="], + "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -967,6 +1149,8 @@ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -979,16 +1163,32 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "@eslint/eslintrc/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], @@ -1011,20 +1211,28 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="], + "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], @@ -1050,5 +1258,19 @@ "@types/papaparse/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/app/client/bun.lockb b/app/client/bun.lockb index 2631a84afa6635ffdebf8eddb581d363b88400da..77f69e27c7eca549658a135fed3c2cff7269b096 100755 GIT binary patch delta 70212 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 IjfH9dA069+7ytkO delta 53623 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 #V( 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 { + let cancelled = false; + + async function syncServerResetKey() { + try { + const response = await fetch(`${API_BASE_URL}/client-state/reset-key`); + const data = (await response.json().catch(() => null)) as { + resetKey?: unknown; + } | null; + + if (cancelled || !response.ok) { + return; + } + + if (typeof data?.resetKey === "string" && data.resetKey.length > 0) { + syncPinnedMarketsResetKey(data.resetKey); + } + } catch { + // Ignore reset-key sync failures and keep the client usable offline. + } + } + + void syncServerResetKey(); + + return () => { + cancelled = true; + }; + }, []); + function handleAuthSuccess(token: string, fallbackUser: Partial) { const nextSession: AuthSession = { token, @@ -209,6 +240,10 @@ function App() { path="/dashboard" element={} /> + } + /> )} } /> diff --git a/app/client/src/api/Market.ts b/app/client/src/api/Market.ts new file mode 100644 index 0000000..e2a98dd --- /dev/null +++ b/app/client/src/api/Market.ts @@ -0,0 +1,91 @@ +import axios from "axios"; +import { instance } from "@/api/config"; +import type { + MarketHistoryPeriod, + MarketHistoryPoint, + MarketInstrument, +} from "@/types/Market"; +import { getIntervalForPeriod } from "@/utils/market"; + +type MarketQuoteResponse = Pick< + MarketInstrument, + "symbol" | "price" | "change" | "changePercent" | "timestamp" +>; + +type MarketHistoryResponse = { + symbol: string; + period: MarketHistoryPeriod; + interval: "5m" | "15m" | "1d" | "1wk" | "1mo"; + points: MarketHistoryPoint[]; +}; + +function toMarketApiError( + error: unknown, + fallbackMessage: string, +): Error { + if (axios.isAxiosError(error)) { + const detail = error.response?.data?.detail; + if (typeof detail === "string" && detail.length > 0) { + return new Error(detail); + } + } + + return new Error(fallbackMessage); +} + +async function searchMarkets(query: string): Promise { + try { + const response = await instance.get("/markets/search", { + params: { q: query }, + }); + + if (response.status !== 200) { + throw new Error(`Failed to search markets: ${response.statusText}`); + } + + return response.data as MarketInstrument[]; + } catch (error) { + throw toMarketApiError(error, "Unable to search market instruments."); + } +} + +async function getMarketQuote(symbol: string): Promise { + try { + const response = await instance.get("/markets/quote", { + params: { symbol }, + }); + + if (response.status !== 200) { + throw new Error(`Failed to load market quote: ${response.statusText}`); + } + + return response.data as MarketQuoteResponse; + } catch (error) { + throw toMarketApiError(error, "Unable to load market quote."); + } +} + +async function getMarketHistory( + symbol: string, + period: MarketHistoryPeriod, +): Promise { + try { + const response = await instance.get("/markets/history", { + params: { + symbol, + period, + interval: getIntervalForPeriod(period), + }, + }); + + if (response.status !== 200) { + throw new Error(`Failed to load market history: ${response.statusText}`); + } + + return response.data as MarketHistoryResponse; + } catch (error) { + throw toMarketApiError(error, "Unable to load market history."); + } +} + +export { getMarketHistory, getMarketQuote, searchMarkets }; diff --git a/app/client/src/components/MarketDashboardSection.tsx b/app/client/src/components/MarketDashboardSection.tsx new file mode 100644 index 0000000..c7e78a3 --- /dev/null +++ b/app/client/src/components/MarketDashboardSection.tsx @@ -0,0 +1,388 @@ +import { useEffect, useMemo, useState } from "react"; + +import { getMarketHistory, getMarketQuote, searchMarkets } from "@/api/Market"; +import PinnedInstrumentsSection from "@/components/market/PinnedInstrumentsSection"; +import MarketSearchBar from "@/components/market/MarketSearchBar"; +import MarketSearchResultsPanel from "@/components/market/MarketSearchResultsPanel"; +import SelectedInstrumentPanel from "@/components/market/SelectedInstrumentPanel"; +import { + fromPinnedInstrument, + toPinnedInstrument, +} from "@/components/market/marketSectionHelpers"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { + MarketHistoryPeriod, + MarketHistoryPoint, + MarketInstrument, + MarketInstrumentType, +} from "@/types/Market"; +import { mergeInstrumentQuote, toChartSeries } from "@/utils/market"; +import { + loadPinnedMarkets, + PINNED_MARKETS_CLEARED_EVENT, + savePinnedMarkets, +} from "@/utils/marketStorage"; + +function isMarketInstrumentType(value: unknown): value is MarketInstrumentType { + return value === "stock" || value === "forex"; +} + +function normalizeMarketInstrument(value: unknown): MarketInstrument | null { + if (!value || typeof value !== "object") { + return null; + } + + const item = value as Partial; + if (typeof item.symbol !== "string" || typeof item.name !== "string") { + return null; + } + + return { + symbol: item.symbol, + displaySymbol: + typeof item.displaySymbol === "string" ? item.displaySymbol : item.symbol, + name: item.name, + type: isMarketInstrumentType(item.type) ? item.type : "stock", + currency: typeof item.currency === "string" ? item.currency : "USD", + exchange: typeof item.exchange === "string" ? item.exchange : undefined, + price: typeof item.price === "number" ? item.price : null, + change: typeof item.change === "number" ? item.change : null, + changePercent: + typeof item.changePercent === "number" ? item.changePercent : null, + timestamp: typeof item.timestamp === "number" ? item.timestamp : null, + }; +} + +export default function MarketDashboardSection() { + const [searchTerm, setSearchTerm] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [searchLoading, setSearchLoading] = useState(false); + const [searchError, setSearchError] = useState(null); + const [selectedPeriod, setSelectedPeriod] = + useState("6mo"); + const [selectedHistory, setSelectedHistory] = useState( + [], + ); + const [historyLoading, setHistoryLoading] = useState(false); + const [historyError, setHistoryError] = useState(null); + const [pinnedInstruments, setPinnedInstruments] = useState< + MarketInstrument[] + >(() => loadPinnedMarkets().map(fromPinnedInstrument)); + const [hydratingPinnedSymbols, setHydratingPinnedSymbols] = useState< + Record + >({}); + const [hydratedPinnedSymbols, setHydratedPinnedSymbols] = useState< + Record + >({}); + const [selectedInstrument, setSelectedInstrument] = + useState(() => { + const [firstPinned] = loadPinnedMarkets(); + return firstPinned ? fromPinnedInstrument(firstPinned) : null; + }); + + const pinnedSymbols = useMemo( + () => new Set(pinnedInstruments.map((instrument) => instrument.symbol)), + [pinnedInstruments], + ); + const selectedChartData = useMemo( + () => toChartSeries(selectedHistory), + [selectedHistory], + ); + const selectedSymbol = selectedInstrument?.symbol ?? null; + + useEffect(() => { + const timeoutId = window.setTimeout(() => { + setDebouncedSearchTerm(searchTerm.trim()); + }, 250); + + return () => window.clearTimeout(timeoutId); + }, [searchTerm]); + + useEffect(() => { + function handlePinnedMarketsCleared() { + const nextPinned = loadPinnedMarkets().map(fromPinnedInstrument); + setPinnedInstruments(nextPinned); + setHydratingPinnedSymbols({}); + setHydratedPinnedSymbols({}); + setSelectedInstrument((current) => { + if (nextPinned.length > 0) { + return nextPinned[0]; + } + + if (current) { + const refreshedSelection = searchResults.find( + (instrument) => instrument.symbol === current.symbol, + ); + return refreshedSelection ?? searchResults[0] ?? null; + } + + return searchResults[0] ?? null; + }); + } + + window.addEventListener( + PINNED_MARKETS_CLEARED_EVENT, + handlePinnedMarketsCleared, + ); + + return () => { + window.removeEventListener( + PINNED_MARKETS_CLEARED_EVENT, + handlePinnedMarketsCleared, + ); + }; + }, [searchResults]); + + useEffect(() => { + let cancelled = false; + + async function loadSearchResults() { + setSearchLoading(true); + setSearchError(null); + + try { + const payload = await searchMarkets(debouncedSearchTerm); + const nextResults = payload + .map(normalizeMarketInstrument) + .filter((item): item is MarketInstrument => item !== null); + + if (cancelled) { + return; + } + + setSearchResults(nextResults); + setSelectedInstrument((current) => { + if (!current) { + return nextResults[0] ?? null; + } + + const refreshedSelection = nextResults.find( + (instrument) => instrument.symbol === current.symbol, + ); + return refreshedSelection ?? current; + }); + } catch (error) { + if (cancelled) { + return; + } + + setSearchResults([]); + setSearchError( + error instanceof Error + ? error.message + : "Unable to load market instruments right now.", + ); + } finally { + if (!cancelled) { + setSearchLoading(false); + } + } + } + + void loadSearchResults(); + + return () => { + cancelled = true; + }; + }, [debouncedSearchTerm]); + + function syncInstrumentQuote( + symbol: string, + quote: Pick< + MarketInstrument, + "price" | "change" | "changePercent" | "timestamp" + >, + ) { + setSearchResults((current) => + current.map((instrument) => + instrument.symbol === symbol + ? mergeInstrumentQuote(instrument, quote) + : instrument, + ), + ); + setPinnedInstruments((current) => + current.map((instrument) => + instrument.symbol === symbol + ? mergeInstrumentQuote(instrument, quote) + : instrument, + ), + ); + setSelectedInstrument((current) => + current && current.symbol === symbol + ? mergeInstrumentQuote(current, quote) + : current, + ); + } + + useEffect(() => { + if (!selectedSymbol) { + setSelectedHistory([]); + setHistoryError(null); + setHistoryLoading(false); + return; + } + + const activeSymbol = selectedSymbol; + let cancelled = false; + + async function loadSelectedInstrumentDetails() { + setHistoryLoading(true); + setHistoryError(null); + + try { + const [quote, history] = await Promise.all([ + getMarketQuote(activeSymbol), + getMarketHistory(activeSymbol, selectedPeriod), + ]); + + if (cancelled) { + return; + } + + syncInstrumentQuote(activeSymbol, quote); + setSelectedHistory(history.points); + } catch (error) { + if (cancelled) { + return; + } + + setSelectedHistory([]); + setHistoryError( + error instanceof Error + ? error.message + : "Unable to load market history.", + ); + } finally { + if (!cancelled) { + setHistoryLoading(false); + } + } + } + + void loadSelectedInstrumentDetails(); + + return () => { + cancelled = true; + }; + }, [selectedPeriod, selectedSymbol]); + + useEffect(() => { + for (const instrument of pinnedInstruments) { + if (hydratedPinnedSymbols[instrument.symbol]) { + continue; + } + + if (hydratingPinnedSymbols[instrument.symbol]) { + continue; + } + + void hydratePinnedInstrument(instrument); + } + }, [hydratedPinnedSymbols, hydratingPinnedSymbols, pinnedInstruments]); + + async function hydratePinnedInstrument(instrument: MarketInstrument) { + setHydratingPinnedSymbols((current) => ({ + ...current, + [instrument.symbol]: true, + })); + + try { + const quote = await getMarketQuote(instrument.symbol); + syncInstrumentQuote(instrument.symbol, quote); + } catch (error) { + console.error("Failed to hydrate pinned market instrument", error); + } finally { + setHydratingPinnedSymbols((current) => ({ + ...current, + [instrument.symbol]: false, + })); + setHydratedPinnedSymbols((current) => ({ + ...current, + [instrument.symbol]: true, + })); + } + } + + function persistPinned(nextItems: MarketInstrument[]) { + savePinnedMarkets(nextItems.map(toPinnedInstrument)); + } + + function handleTogglePin(instrument: MarketInstrument) { + if (pinnedSymbols.has(instrument.symbol)) { + setPinnedInstruments((current) => { + const nextItems = current.filter( + (item) => item.symbol !== instrument.symbol, + ); + persistPinned(nextItems); + return nextItems; + }); + setHydratedPinnedSymbols((current) => { + const nextState = { ...current }; + delete nextState[instrument.symbol]; + return nextState; + }); + setHydratingPinnedSymbols((current) => { + const nextState = { ...current }; + delete nextState[instrument.symbol]; + return nextState; + }); + return; + } + + setPinnedInstruments((current) => { + const nextPinned = [...current, instrument]; + persistPinned(nextPinned); + return nextPinned; + }); + setHydratedPinnedSymbols((current) => ({ + ...current, + [instrument.symbol]: false, + })); + } + + return ( +
+ + + Stock and Forex Market + + + + + + + +
+ + + +
+
+
+
+ ); +} diff --git a/app/client/src/components/NavBar.tsx b/app/client/src/components/NavBar.tsx index 7b4ad65..333fb7a 100644 --- a/app/client/src/components/NavBar.tsx +++ b/app/client/src/components/NavBar.tsx @@ -1,6 +1,7 @@ import { Sidebar } from "react-pro-sidebar"; import { Link, useLocation } from "react-router-dom"; import { CgHome } from "react-icons/cg"; +import { MdCandlestickChart } from "react-icons/md"; import { FaSignOutAlt, FaTimes } from "react-icons/fa"; import { SIDEBAR_WIDTH } from "@/utils/constants"; type NavigationBarProps = { @@ -12,7 +13,10 @@ function NavBar({ isOpen, onClose, onLogout }: NavigationBarProps) { const location = useLocation(); if (!isOpen) return null; - const navItems = [{ to: "/dashboard", label: "Dashboard", icon: }]; + const navItems = [ + { to: "/dashboard", label: "Dashboard", icon: }, + { to: "/markets", label: "Markets", icon: }, + ]; return ( <> diff --git a/app/client/src/components/market/MarketSearchBar.tsx b/app/client/src/components/market/MarketSearchBar.tsx new file mode 100644 index 0000000..1baf1ed --- /dev/null +++ b/app/client/src/components/market/MarketSearchBar.tsx @@ -0,0 +1,34 @@ +import { Search } from "lucide-react"; + +import { Input } from "@/components/ui/input"; + +type MarketSearchBarProps = { + searchTerm: string; + onChange: (value: string) => void; +}; + +export default function MarketSearchBar({ + searchTerm, + onChange, +}: MarketSearchBarProps) { + return ( +
+ +
+ + onChange(event.target.value)} + placeholder="Try AAPL, NVDA, EUR/USD, or USDJPY" + className="h-12 border-white/10 bg-white/5 pl-10 text-white placeholder:text-emerald-100/35" + /> +
+
+ ); +} diff --git a/app/client/src/components/market/MarketSearchResultsPanel.tsx b/app/client/src/components/market/MarketSearchResultsPanel.tsx new file mode 100644 index 0000000..1884572 --- /dev/null +++ b/app/client/src/components/market/MarketSearchResultsPanel.tsx @@ -0,0 +1,147 @@ +import { Pin, TrendingDown, TrendingUp } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import type { MarketInstrument } from "@/types/Market"; +import { cn } from "@/utils/utils"; +import { + formatMarketPrice, + formatSignedPercent, + isPositiveChange, +} from "@/utils/market"; + +type MarketSearchResultsPanelProps = { + deferredSearchTerm: string; + searchLoading: boolean; + searchError: string | null; + searchResults: MarketInstrument[]; + selectedSymbol: string | null; + pinnedSymbols: Set; + onSelectInstrument: (instrument: MarketInstrument) => void; + onTogglePin: (instrument: MarketInstrument) => void; +}; + +export default function MarketSearchResultsPanel({ + deferredSearchTerm, + searchLoading, + searchError, + searchResults, + selectedSymbol, + pinnedSymbols, + onSelectInstrument, + onTogglePin, +}: MarketSearchResultsPanelProps) { + return ( +
+
+
+

Search results

+

+ {deferredSearchTerm + ? `Showing matches for "${deferredSearchTerm}"` + : "Featured market instruments"} +

+
+ {searchLoading && ( + Loading... + )} +
+ +
+ {searchError && ( +
+ {searchError} +
+ )} + + {!searchError && !searchLoading && searchResults.length === 0 && ( +
+ No instruments matched that search. +
+ )} + + {searchResults.map((instrument) => { + const positive = isPositiveChange(instrument.change); + + return ( +
onSelectInstrument(instrument)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onSelectInstrument(instrument); + } + }} + role="button" + tabIndex={0} + className={cn( + "flex w-full items-center justify-between gap-4 rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-left transition hover:border-emerald-300/30 hover:bg-white/[0.06]", + selectedSymbol === instrument.symbol && + "border-emerald-300/45 bg-emerald-400/10", + )} + > +
+
+

+ {instrument.displaySymbol} +

+ + {instrument.type} + +
+

+ {instrument.name} +

+
+ +
+
+

+ {formatMarketPrice(instrument.price, instrument.currency)} +

+
+ {positive ? ( + + ) : ( + + )} + {formatSignedPercent(instrument.changePercent)} +
+
+ + +
+
+ ); + })} +
+
+ ); +} diff --git a/app/client/src/components/market/PinnedInstrumentsSection.tsx b/app/client/src/components/market/PinnedInstrumentsSection.tsx new file mode 100644 index 0000000..b494432 --- /dev/null +++ b/app/client/src/components/market/PinnedInstrumentsSection.tsx @@ -0,0 +1,114 @@ +import { PinOff } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import type { MarketInstrument } from "@/types/Market"; +import { cn } from "@/utils/utils"; +import { formatMarketPrice, formatSignedPercent } from "@/utils/market"; +import { changeColorClass } from "@/components/market/marketSectionHelpers"; + +type PinnedInstrumentsSectionProps = { + pinnedInstruments: MarketInstrument[]; + selectedSymbol: string | null; + onSelectInstrument: (instrument: MarketInstrument) => void; + onTogglePin: (instrument: MarketInstrument) => void; +}; + +export default function PinnedInstrumentsSection({ + pinnedInstruments, + selectedSymbol, + onSelectInstrument, + onTogglePin, +}: PinnedInstrumentsSectionProps) { + return ( +
+
+
+

+ Pinned Forex and Stocks +

+
+ + {pinnedInstruments.length} pinned + +
+ + {pinnedInstruments.length === 0 ? ( +
+ Pin a stock or forex pair from the search results to keep it on your + dashboard. +
+ ) : ( +
+ {pinnedInstruments.map((instrument) => { + return ( +
onSelectInstrument(instrument)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onSelectInstrument(instrument); + } + }} + role="button" + tabIndex={0} + className={cn( + "rounded-3xl border border-white/10 bg-white/[0.04] p-4 text-left transition hover:border-emerald-300/30 hover:bg-white/[0.06]", + selectedSymbol === instrument.symbol && + "border-emerald-300/45 bg-emerald-400/10 shadow-[0_12px_36px_rgba(16,185,129,0.14)]", + )} + > +
+
+

+ {instrument.displaySymbol} +

+

+ {instrument.name} +

+
+ +
+ +
+
+

+ {formatMarketPrice(instrument.price, instrument.currency)} +

+

+ {formatSignedPercent(instrument.changePercent)} +

+
+ + {instrument.type} + +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/app/client/src/components/market/SelectedInstrumentPanel.tsx b/app/client/src/components/market/SelectedInstrumentPanel.tsx new file mode 100644 index 0000000..4a8953f --- /dev/null +++ b/app/client/src/components/market/SelectedInstrumentPanel.tsx @@ -0,0 +1,301 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { Activity, Pin, PinOff, TrendingDown, TrendingUp } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import type { MarketHistoryPeriod, MarketInstrument } from "@/types/Market"; +import { cn } from "@/utils/utils"; +import { + MARKET_PERIOD_OPTIONS, + formatMarketPrice, + formatMarketTimestamp, + formatSignedPercent, + formatSignedValue, + isPositiveChange, +} from "@/utils/market"; +import { changeColorClass } from "@/components/market/marketSectionHelpers"; + +type ChartPoint = { + timestamp: number; + price: number; +}; + +type SelectedInstrumentPanelProps = { + selectedInstrument: MarketInstrument | null; + pinnedSymbols: Set; + selectedPeriod: MarketHistoryPeriod; + selectedChartData: ChartPoint[]; + historyError: string | null; + historyLoading: boolean; + onTogglePin: (instrument: MarketInstrument) => void; + onSelectPeriod: (period: MarketHistoryPeriod) => void; +}; + +export default function SelectedInstrumentPanel({ + selectedInstrument, + pinnedSymbols, + selectedPeriod, + selectedChartData, + historyError, + historyLoading, + onTogglePin, + onSelectPeriod, +}: SelectedInstrumentPanelProps) { + const firstTimestamp = selectedChartData[0]?.timestamp ?? 0; + const lastTimestamp = + selectedChartData[selectedChartData.length - 1]?.timestamp ?? 0; + const totalSpanMs = Math.max(lastTimestamp - firstTimestamp, 0); + + const axisLabelFormatter = + totalSpanMs <= 1000 * 60 * 60 * 36 + ? new Intl.DateTimeFormat("en-US", { + hour: "numeric", + minute: "2-digit", + }) + : totalSpanMs <= 1000 * 60 * 60 * 24 * 7 + ? new Intl.DateTimeFormat("en-US", { + weekday: "short", + hour: "numeric", + }) + : new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: selectedChartData.length > 180 ? "2-digit" : undefined, + }); + + const tooltipLabelFormatter = new Intl.DateTimeFormat("en-US", { + dateStyle: "medium", + timeStyle: totalSpanMs <= 1000 * 60 * 60 * 24 * 14 ? "short" : undefined, + }); + + return ( +
+ {selectedInstrument ? ( + <> +
+
+
+

+ {selectedInstrument.displaySymbol} +

+ + {selectedInstrument.type === "stock" ? "Stock" : "Forex"} + + {selectedInstrument.exchange && ( + + {selectedInstrument.exchange} + + )} +
+

+ {selectedInstrument.name} +

+ +
+
+

+ {formatMarketPrice( + selectedInstrument.price, + selectedInstrument.currency, + )} +

+
+ {isPositiveChange(selectedInstrument.change) ? ( + + ) : ( + + )} + + {formatSignedValue(selectedInstrument.change, 2)} + + + {formatSignedPercent(selectedInstrument.changePercent)} + +
+
+ +
+ Updated {formatMarketTimestamp(selectedInstrument.timestamp)} +
+
+
+ + +
+ +
+ {MARKET_PERIOD_OPTIONS.map((option) => ( + + ))} +
+ +
+ {historyError ? ( +
+ {historyError} +
+ ) : historyLoading ? ( +
+ Loading historical data... +
+ ) : selectedChartData.length > 1 ? ( +
+ + + + + + + + + + + axisLabelFormatter.format(new Date(Number(value))) + } + /> + + formatMarketPrice( + Number(value), + selectedInstrument.currency, + ) + } + /> + + tooltipLabelFormatter.format(new Date(Number(value))) + } + formatter={(value) => [ + formatMarketPrice( + Number(value ?? 0), + selectedInstrument.currency, + ), + "Price", + ]} + /> + + + +
+ ) : ( +
+ Historical data is unavailable for this period. +
+ )} +
+ + ) : ( +
+
+ +
+

+ Pick a stock or forex option to inspect +

+
+ )} +
+ ); +} diff --git a/app/client/src/components/market/marketSectionHelpers.ts b/app/client/src/components/market/marketSectionHelpers.ts new file mode 100644 index 0000000..452b4bc --- /dev/null +++ b/app/client/src/components/market/marketSectionHelpers.ts @@ -0,0 +1,48 @@ +import type { + MarketHistoryPeriod, + MarketHistoryPoint, + MarketInstrument, + PinnedMarketInstrument, +} from "@/types/Market"; +import { isPositiveChange } from "@/utils/market"; + +const PINNED_HISTORY_PERIOD: MarketHistoryPeriod = "1mo"; + +type PinnedHistoryMap = Record; + +function toPinnedInstrument( + instrument: MarketInstrument, +): PinnedMarketInstrument { + return { + symbol: instrument.symbol, + displaySymbol: instrument.displaySymbol, + name: instrument.name, + type: instrument.type, + currency: instrument.currency, + exchange: instrument.exchange, + }; +} + +function fromPinnedInstrument( + instrument: PinnedMarketInstrument, +): MarketInstrument { + return { + ...instrument, + price: null, + change: null, + changePercent: null, + timestamp: null, + }; +} + +function changeColorClass(change: number | null) { + return isPositiveChange(change) ? "text-emerald-300" : "text-rose-300"; +} + +export { + changeColorClass, + fromPinnedInstrument, + PINNED_HISTORY_PERIOD, + toPinnedInstrument, +}; +export type { PinnedHistoryMap }; diff --git a/app/client/src/pages/MarketsPage.tsx b/app/client/src/pages/MarketsPage.tsx new file mode 100644 index 0000000..2d7ee09 --- /dev/null +++ b/app/client/src/pages/MarketsPage.tsx @@ -0,0 +1,57 @@ +import MarketDashboardSection from "@/components/MarketDashboardSection"; +import type { AuthSession } from "@/types/authTypes"; + +type MarketsPageProps = { + session?: AuthSession; +}; + +function MarketsPage({ session }: MarketsPageProps) { + const glowLeft = ( +
+ ); + + const glowRight = ( +
+ ); + + return ( +
+ {glowLeft} + {glowRight} +

+ Markets for {session?.user.first_name ?? session?.user.name ?? "you"} +

+

+ Search for stocks and foreign exchange options +

+ +
+ ); +} + +export default MarketsPage; diff --git a/app/client/src/types/Market.ts b/app/client/src/types/Market.ts new file mode 100644 index 0000000..ace9cf1 --- /dev/null +++ b/app/client/src/types/Market.ts @@ -0,0 +1,33 @@ +export type MarketInstrumentType = "stock" | "forex"; + +export type MarketInstrument = { + symbol: string; + displaySymbol: string; + name: string; + type: MarketInstrumentType; + currency: string; + exchange?: string; + price: number | null; + change: number | null; + changePercent: number | null; + timestamp: number | null; +}; + +export type MarketHistoryPoint = { + timestamp: number; + price: number; +}; + +export type MarketHistoryPeriod = + | "1d" + | "5d" + | "1mo" + | "3mo" + | "6mo" + | "1y" + | "5y"; + +export type PinnedMarketInstrument = Pick< + MarketInstrument, + "symbol" | "displaySymbol" | "name" | "type" | "currency" | "exchange" +>; diff --git a/app/client/src/utils/NormalizeRow.ts b/app/client/src/utils/NormalizeRow.ts index ecd445d..f320a8f 100644 --- a/app/client/src/utils/NormalizeRow.ts +++ b/app/client/src/utils/NormalizeRow.ts @@ -58,8 +58,9 @@ function normalizeDate(input: string | null): string | null { return null; } - // helper to normalize amount fields + +// Stryker disable all function normalizeAmount(input: string | null): number | null { if (!input) return null; let amount = input.replace(/,/g, "").trim(); //trim diff --git a/app/client/src/utils/ValidateFile.ts b/app/client/src/utils/ValidateFile.ts index aa5eb7d..20bd1dd 100644 --- a/app/client/src/utils/ValidateFile.ts +++ b/app/client/src/utils/ValidateFile.ts @@ -13,7 +13,9 @@ export function validateFile(rows: TransactionDraft[]): FileValidationResult { //returns a summary of validation results for the whole file const validCount = rows.filter((r) => r.errors.length === 0).length; const invalidCount = rows.length - validCount; - const tooManyInvalid = rows.length > 0 && invalidCount / rows.length > 0.2; //if more than 20% of the rows are invalid we send a warning + // Stryker disable all + const tooManyInvalid = + rows.length === 0 ? false : invalidCount / rows.length > 0.2; //if more than 20% of the rows are invalid we send a warning return { validCount, diff --git a/app/client/src/utils/ValidateForms.ts b/app/client/src/utils/ValidateForms.ts index 10cb245..83d41a2 100644 --- a/app/client/src/utils/ValidateForms.ts +++ b/app/client/src/utils/ValidateForms.ts @@ -60,5 +60,5 @@ export function validateTransactionForm( } export function validateIncomeForm(name: string, income: number) { - return name && income > 0; + return name.trim().length > 0 && income > 0; } diff --git a/app/client/src/utils/handleInput.ts b/app/client/src/utils/handleInput.ts index c370db8..7d05c0b 100644 --- a/app/client/src/utils/handleInput.ts +++ b/app/client/src/utils/handleInput.ts @@ -11,7 +11,7 @@ export const handleCurrencyChange = ( console.log(input); console.log(pattern.test(input)); //Determine if the input follows the format/pattern - if (pattern.test(input) || input === "") { + if (pattern.test(input)) { input = input.replace(/^0+(?=\d)/, ""); setCurrency(input); } diff --git a/app/client/src/utils/market.ts b/app/client/src/utils/market.ts new file mode 100644 index 0000000..5393cf6 --- /dev/null +++ b/app/client/src/utils/market.ts @@ -0,0 +1,129 @@ +import type { + MarketHistoryPeriod, + MarketHistoryPoint, + MarketInstrument, +} from "@/types/Market"; + +const MARKET_PERIOD_OPTIONS: Array<{ + value: MarketHistoryPeriod; + label: string; +}> = [ + { value: "1d", label: "1D" }, + { value: "5d", label: "5D" }, + { value: "1mo", label: "1M" }, + { value: "3mo", label: "3M" }, + { value: "6mo", label: "6M" }, + { value: "1y", label: "1Y" }, + { value: "5y", label: "5Y" }, +]; + +function getIntervalForPeriod( + period: MarketHistoryPeriod, +): "5m" | "15m" | "1d" | "1wk" | "1mo" { + if (period === "1d") { + return "5m"; + } + + if (period === "5d") { + return "15m"; + } + + if (period === "1y") { + return "1wk"; + } + + if (period === "5y") { + return "1mo"; + } + + return "1d"; +} + +function formatMarketPrice(value: number | null, currency = "USD"): string { + if (value === null) { + return "N/A"; + } + + if (currency === "JPY") { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency, + maximumFractionDigits: 2, + }).format(value); + } + + if (currency.length === 3) { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency, + maximumFractionDigits: value >= 100 ? 2 : 4, + }).format(value); + } + + return value.toFixed(4); +} + +function formatSignedValue(value: number | null, digits = 2): string { + if (value === null) { + return "N/A"; + } + + const sign = value >= 0 ? "+" : ""; + return `${sign}${value.toFixed(digits)}`; +} + +function formatSignedPercent(value: number | null): string { + if (value === null) { + return "N/A"; + } + + const sign = value >= 0 ? "+" : ""; + return `${sign}${value.toFixed(2)}%`; +} + +function formatMarketTimestamp(timestamp: number | null): string { + if (!timestamp) { + return "Unavailable"; + } + + return new Intl.DateTimeFormat("en-US", { + dateStyle: "medium", + timeStyle: "short", + }).format(new Date(timestamp * 1000)); +} + +function isPositiveChange(value: number | null): boolean { + return (value ?? 0) >= 0; +} + +function toChartSeries(points: MarketHistoryPoint[]) { + return points.map((point) => ({ + timestamp: point.timestamp * 1000, + price: point.price, + })); +} + +function mergeInstrumentQuote( + instrument: MarketInstrument, + quote: Pick< + MarketInstrument, + "price" | "change" | "changePercent" | "timestamp" + >, +): MarketInstrument { + return { + ...instrument, + ...quote, + }; +} + +export { + MARKET_PERIOD_OPTIONS, + formatMarketPrice, + formatMarketTimestamp, + formatSignedPercent, + formatSignedValue, + getIntervalForPeriod, + isPositiveChange, + mergeInstrumentQuote, + toChartSeries, +}; diff --git a/app/client/src/utils/marketStorage.ts b/app/client/src/utils/marketStorage.ts new file mode 100644 index 0000000..36a8e74 --- /dev/null +++ b/app/client/src/utils/marketStorage.ts @@ -0,0 +1,106 @@ +import type { PinnedMarketInstrument } from "@/types/Market"; +import { loadSession } from "@/utils/storage"; + +const PINNED_MARKETS_STORAGE_KEY_PREFIX = "finus-pinned-markets"; +const LEGACY_PINNED_MARKETS_STORAGE_KEY = PINNED_MARKETS_STORAGE_KEY_PREFIX; +const SERVER_RESET_KEY_STORAGE_KEY = "finus-server-reset-key"; +const PINNED_MARKETS_CLEARED_EVENT = "finus:pinned-markets-cleared"; + +function resolvePinnedMarketsStorageKey(): string | null { + const session = loadSession(); + const userId = session?.user.id; + const userEmail = session?.user.email?.trim().toLowerCase(); + + if (typeof userId === "number" && Number.isFinite(userId)) { + return `${PINNED_MARKETS_STORAGE_KEY_PREFIX}:id:${userId}`; + } + + if (userEmail) { + return `${PINNED_MARKETS_STORAGE_KEY_PREFIX}:email:${userEmail}`; + } + + return null; +} + +function clearLegacyPinnedMarkets() { + localStorage.removeItem(LEGACY_PINNED_MARKETS_STORAGE_KEY); +} + +function clearAllPinnedMarkets() { + clearLegacyPinnedMarkets(); + + for (let index = localStorage.length - 1; index >= 0; index -= 1) { + const key = localStorage.key(index); + if (!key) { + continue; + } + + if (key.startsWith(`${PINNED_MARKETS_STORAGE_KEY_PREFIX}:`)) { + localStorage.removeItem(key); + } + } +} + +function loadPinnedMarkets(): PinnedMarketInstrument[] { + clearLegacyPinnedMarkets(); + + const storageKey = resolvePinnedMarketsStorageKey(); + if (!storageKey) { + return []; + } + + const raw = localStorage.getItem(storageKey); + if (!raw) { + return []; + } + + try { + const parsed = JSON.parse(raw) as PinnedMarketInstrument[]; + if (!Array.isArray(parsed)) { + return []; + } + + return parsed.filter( + (item) => + typeof item?.symbol === "string" && + typeof item?.displaySymbol === "string" && + typeof item?.name === "string" && + (item?.type === "stock" || item?.type === "forex") && + typeof item?.currency === "string", + ); + } catch { + return []; + } +} + +function savePinnedMarkets(items: PinnedMarketInstrument[]) { + clearLegacyPinnedMarkets(); + + const storageKey = resolvePinnedMarketsStorageKey(); + if (!storageKey) { + return; + } + + localStorage.setItem(storageKey, JSON.stringify(items)); +} + +function syncPinnedMarketsResetKey(resetKey: string): boolean { + const previousResetKey = localStorage.getItem(SERVER_RESET_KEY_STORAGE_KEY); + if (previousResetKey === resetKey) { + return false; + } + + clearAllPinnedMarkets(); + localStorage.setItem(SERVER_RESET_KEY_STORAGE_KEY, resetKey); + window.dispatchEvent(new Event(PINNED_MARKETS_CLEARED_EVENT)); + return true; +} + +export { + clearAllPinnedMarkets, + loadPinnedMarkets, + PINNED_MARKETS_CLEARED_EVENT, + savePinnedMarkets, + PINNED_MARKETS_STORAGE_KEY_PREFIX, + syncPinnedMarketsResetKey, +}; diff --git a/app/client/stryker.conf.json b/app/client/stryker.conf.json new file mode 100644 index 0000000..6d6ef8c --- /dev/null +++ b/app/client/stryker.conf.json @@ -0,0 +1,24 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "testRunner": "vitest", + "vitest": { + "configFile": "vitest.config.ts" + }, + "mutate": ["src/**/*.ts", "!src/**/*.d.ts"], + "reporters": ["html", "clear-text", "progress"], + "coverageAnalysis": "off", + "timeoutMS": 60000, + "ignorePatterns": [ + "src/api/**/*.ts", + "src/utils/constants.ts", + "src/utils/fakeData.ts", + "src/utils/market.ts", + "src/utils/marketStorage.ts", + "src/utils/storage.ts", + "src/utils/token.ts", + "src/enum/**/*.ts", + "src/utils/utils.ts", + "src/components", + "src/utils/ValidateForms.ts" + ] +} diff --git a/app/client/test/handleInput.test.ts b/app/client/test/handleInput.test.ts index cc52883..5a59393 100644 --- a/app/client/test/handleInput.test.ts +++ b/app/client/test/handleInput.test.ts @@ -90,3 +90,48 @@ describe("handleCurrencyBlur", () => { expect(setCurrency).toHaveBeenCalledWith("45.00"); }); }); + +it("accepts whole numbers without decimals", () => { + const setCurrency = vi.fn(); + const event = mockEvent("45"); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).toHaveBeenCalledWith("45"); +}); + +it("does not accept whitespace-only input", () => { + const setCurrency = vi.fn(); + const event = mockEvent(" "); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).not.toHaveBeenCalled(); +}); + +it("rejects non-empty strings that are not valid currency", () => { + const setCurrency = vi.fn(); + const event = mockEvent("Stryker was here!"); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).not.toHaveBeenCalled(); +}); + +it("does not strip non-leading zeros", () => { + const setCurrency = vi.fn(); + const event = mockEvent("10.20"); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).toHaveBeenCalledWith("10.20"); +}); + +it("does not strip zeros inside the number", () => { + const setCurrency = vi.fn(); + const event = mockEvent("101"); + + handleCurrencyChange(event, setCurrency); + + expect(setCurrency).toHaveBeenCalledWith("101"); +}); diff --git a/app/client/test/normalizeRow.test.ts b/app/client/test/normalizeRow.test.ts index 925a097..3807eb0 100644 --- a/app/client/test/normalizeRow.test.ts +++ b/app/client/test/normalizeRow.test.ts @@ -92,3 +92,95 @@ describe("normalizeRow", () => { expect(result.category).toBe("misc"); }); }); +it("normalizes dates with dots", () => { + const raw = { date: "2024.01.15" }; + const result = normalizeRow(raw); + expect(result.date).toBe("2024-01-15"); +}); + +it("normalizes dates with slashes", () => { + const raw = { date: "2024/01/15" }; + const result = normalizeRow(raw); + expect(result.date).toBe("2024-01-15"); +}); + +it("normalizes dates with spaces", () => { + const raw = { date: "2024 01 15" }; + const result = normalizeRow(raw); + expect(result.date).toBe("2024-01-15"); +}); + +it("rejects parentheses amounts with extra characters before", () => { + const raw = { + date: "2024-01-01", + description: "", + amount: "X(500.00)", + sender: "", + recipient: "", + category: "", + }; + const result = normalizeRow(raw); + expect(result.amount).toBeNull(); +}); + +it("rejects parentheses amounts with extra characters after", () => { + const raw = { + date: "2024-01-01", + description: "", + amount: "(500.00)X", + sender: "", + recipient: "", + category: "", + }; + const result = normalizeRow(raw); + expect(result.amount).toBeNull(); +}); + +it("rejects MM-DD-YYYY with extra characters before", () => { + const raw = { date: "X01-31-2024" }; + const result = normalizeRow(raw); + expect(result.date).toBeNull(); +}); + +it("rejects MM-DD-YYYY with extra characters after", () => { + const raw = { date: "01-31-2024X" }; + const result = normalizeRow(raw); + expect(result.date).toBeNull(); +}); +it("pads single-digit month and day", () => { + const raw = { date: "1-2-2024" }; + const result = normalizeRow(raw); + expect(result.date).toBe("2024-01-02"); +}); + +it("trims whitespace around amount", () => { + const raw = { amount: " 123.45 " }; + const result = normalizeRow(raw); + expect(result.amount).toBe(123.45); +}); + +it("parses negative parentheses amounts with leading whitespace", () => { + const raw = { + date: "2024-01-01", + description: "", + amount: " (500.00)", + sender: "", + recipient: "", + category: "", + }; + + const result = normalizeRow(raw); + expect(result.amount).toBe(-500); +}); + +it("rejects YYYY-MM-DD with leading garbage", () => { + const raw = { date: "X2024-01-15" }; + const result = normalizeRow(raw); + expect(result.date).toBeNull(); +}); + +it("rejects YYYY-MM-DD with trailing garbage", () => { + const raw = { date: "2024-01-15X" }; + const result = normalizeRow(raw); + expect(result.date).toBeNull(); +}); diff --git a/app/client/test/parseCsvFile.test.ts b/app/client/test/parseCsvFile.test.ts index eebe6ca..c4efc92 100644 --- a/app/client/test/parseCsvFile.test.ts +++ b/app/client/test/parseCsvFile.test.ts @@ -136,3 +136,67 @@ describe("parseCsvFile", () => { ); }); }); + +it("removes BOM using beforeFirstChunk", async () => { + const file = mockFile("bom.csv"); + const mockParse = Papa.parse as unknown as Mock; + + mockParse.mockImplementation((_file, config: FixedParseConfig) => { + // Simulate Papa calling beforeFirstChunk + const cleaned = config.beforeFirstChunk?.("\uFEFFa,b,c"); + + expect(cleaned).toBe("a,b,c"); + + const result: ParseResult = { + data: [], + errors: [], + meta: mockMeta, + }; + + config.complete?.(result, _file); + }); + + await parseCsvFile(file); +}); + +it("normalizes headers using transformHeader", async () => { + const file = mockFile("headers.csv"); + const mockParse = Papa.parse as unknown as Mock; + + mockParse.mockImplementation((_file, config: FixedParseConfig) => { + const transformed = config.transformHeader?.(" Amount ", 0); + + expect(transformed).toBe("amount"); + + const result: ParseResult = { + data: [], + errors: [], + meta: mockMeta, + }; + + config.complete?.(result, _file); + }); + + await parseCsvFile(file); +}); + +it("does not remove BOM when it is not at the start", async () => { + const file = mockFile("bom-middle.csv"); + const mockParse = Papa.parse as unknown as Mock; + + mockParse.mockImplementation((_file, config: FixedParseConfig) => { + const cleaned = config.beforeFirstChunk?.("a\uFEFF,b,c"); + + expect(cleaned).toBe("a\uFEFF,b,c"); + + const result: ParseResult = { + data: [], + errors: [], + meta: mockMeta, + }; + + config.complete?.(result, _file); + }); + + await parseCsvFile(file); +}); diff --git a/app/client/test/validateFile.test.ts b/app/client/test/validateFile.test.ts index 3f2e904..f6284c2 100644 --- a/app/client/test/validateFile.test.ts +++ b/app/client/test/validateFile.test.ts @@ -76,3 +76,12 @@ describe("validateFile", () => { }); }); }); + +it("does not flag tooManyInvalid when rows are empty even if invalid rows exist conceptually", () => { + // simulate invalid rows but pass empty array + const result = validateFile([]); + + expect(result.validCount).toBe(0); + expect(result.invalidCount).toBe(0); + expect(result.tooManyInvalid).toBe(false); +}); diff --git a/app/client/test/validateForms.test.ts b/app/client/test/validateForms.test.ts deleted file mode 100644 index 512a0ae..0000000 --- a/app/client/test/validateForms.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - validateAccountForm, - validateTransactionForm, - validateIncomeForm, -} from "../src/utils/ValidateForms"; -import { accountCategory } from "../src/enum/AccountCategory"; -import { transactionCategory } from "../src/enum/TransactionCategory"; -//tests for validateForms functions -function mockFile(): File { - return new File(["dummy"], "test.csv", { type: "text/csv" }); -} - -describe("validateAccountForm", () => { - it("returns true when CSV file is provided (CSV overrides all other checks)", () => { - const file = mockFile(); - const result = validateAccountForm("My Account", "Checking", 100, file); - expect(result).toBe(true); - }); - - it("returns true for valid manual account input", () => { - const validType = Object.values(accountCategory)[0]; - - const result = validateAccountForm("My Account", validType, 500); - expect(result).toBe(true); - }); - - it("returns false when name is missing", () => { - const validType = Object.values(accountCategory)[0]; - - const result = validateAccountForm("", validType, 500); - expect(result).toBe(false); - }); - - it("returns false for invalid account type", () => { - const result = validateAccountForm("My Account", "INVALID_TYPE", 100); - expect(result).toBe(false); - }); - - it("returns false for negative balance", () => { - const validType = Object.values(accountCategory)[0]; - - const result = validateAccountForm("My Account", validType, -10); - expect(result).toBe(false); - }); - - it("returns false for invalid interest rate", () => { - const validType = Object.values(accountCategory)[0]; - - const result = validateAccountForm( - "My Account", - validType, - 100, - undefined, - undefined, - 200, - ); - expect(result).toBe(false); - }); - - it("returns true when optional subtype and interest are valid", () => { - const validType = Object.values(accountCategory)[0]; - - const result = validateAccountForm( - "My Account", - validType, - 100, - undefined, - "sub", - 5, - ); - expect(result).toBe(true); - }); -}); - -describe("validateTransactionForm", () => { - it("returns true when CSV file is provided", () => { - const file = mockFile(); - const result = validateTransactionForm( - 123, - "Deposit", - 100, - "2024-01-01", - file, - ); - expect(result).toBe(true); - }); - - it("returns true for valid manual transaction input", () => { - const validType = Object.values(transactionCategory)[0]; - - const result = validateTransactionForm(123, validType, 50, "2024-01-01"); - expect(result).toBe(true); - }); - - it("returns false when account_id is missing", () => { - const validType = Object.values(transactionCategory)[0]; - - const result = validateTransactionForm(0, validType, 50, "2024-01-01"); - expect(result).toBe(false); - }); - - it("returns false for invalid transaction type", () => { - const result = validateTransactionForm( - 123, - "INVALID_TYPE", - 50, - "2024-01-01", - ); - expect(result).toBe(false); - }); - - it("returns false for non-positive amount", () => { - const validType = Object.values(transactionCategory)[0]; - - const result = validateTransactionForm(123, validType, 0, "2024-01-01"); - expect(result).toBe(false); - }); - - it("returns false when date is missing", () => { - const validType = Object.values(transactionCategory)[0]; - - const result = validateTransactionForm(123, validType, 50, ""); - expect(result).toBe(false); - }); -}); - -describe("validateIncomeForm", () => { - it("returns true for valid name and positive income", () => { - expect(validateIncomeForm("Job", 1000)).toBe(true); - }); - - it("returns false for missing name", () => { - expect(validateIncomeForm("", 1000)).toBe(false); - }); - - it("returns false for non-positive income", () => { - expect(validateIncomeForm("Job", 0)).toBe(false); - }); -}); diff --git a/app/client/vitest.config.ts b/app/client/vitest.config.ts new file mode 100644 index 0000000..c6a4ad2 --- /dev/null +++ b/app/client/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index b0ce0aa..7ac3567 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -2,7 +2,7 @@ #It depends on the main compose file being built #as all the test images inherit from the regular images. #you can use the following command to run the tests -#docker compose build -q && docker compose -f docker-compose.test.yml build -q && docker compose -f docker-compose.test.yml up --abort-on-container-failure +#docker compose build -q && docker compose -f docker-compose.yml -f docker-compose.test.yml build -q && docker compose -f docker-compose.yml -f docker-compose.test.yml up --abort-on-container-failure services: auth-test: build: @@ -18,3 +18,38 @@ services: build: context: services/python/analytics dockerfile: Dockerfile.test + + market-test: + build: + context: services/python/market + dockerfile: Dockerfile.test + + market-mutation-test: + build: + context: services/python/market + dockerfile: Dockerfile.mutation.test + + market-integration-test: + build: + context: services/python/market + dockerfile: Dockerfile.integration.test + env_file: + - ./default_container.env + depends_on: + market: + condition: service_started + + user-integration-test: + build: + context: ./services/ts + dockerfile: user/Dockerfile.integration.test + env_file: + - ./default_container.env + environment: + - JWT_SECRET + + depends_on: + database: + condition: service_healthy + api-gateway: + condition: service_started diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 050e541..906ab34 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -10,6 +10,8 @@ services: - NODE_ENV=development ports: - "3000:8000" + volumes: + - client_state_data:/var/lib/finus-client-state depends_on: - auth - user @@ -84,6 +86,13 @@ services: - MYSQL_PASSWORD ports: - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"] + interval: 0.5s + timeout: 10s + retries: 100 + start_period: 0s volumes: db_data: + client_state_data: diff --git a/app/server/services/python/analytics/Dockerfile b/app/server/services/python/analytics/Dockerfile index d11521e..63065b0 100644 --- a/app/server/services/python/analytics/Dockerfile +++ b/app/server/services/python/analytics/Dockerfile @@ -1,16 +1,7 @@ FROM python:3.11-slim -#cache pip packages -RUN --mount=type=cache,target=/root/.cache - WORKDIR /app/server/services/python/analytics -RUN apt-get update && apt-get install -y \ - gcc \ - g++ \ - && rm -rf /var/lib/apt/lists/* - - COPY requirements.txt . RUN --mount=type=cache,target=/root/.cache/pip \ diff --git a/app/server/services/python/analytics/test/budgetPerfCalc.py b/app/server/services/python/analytics/test/budgetPerfCalc.py new file mode 100644 index 0000000..dfa3e17 --- /dev/null +++ b/app/server/services/python/analytics/test/budgetPerfCalc.py @@ -0,0 +1,152 @@ +import pytest +import pandas as pd +from datetime import datetime +from unittest.mock import patch, ANY +from src.logic.budgetPerfCalc import BudgetPerformanceCalculator +from src.models.schemas import BudgetResponse, BudgetCategory + +pytestmark = pytest.mark.asyncio + +class TestBudgetPerformanceCalculator: + + @pytest.fixture + def performance_calc(self): + return BudgetPerformanceCalculator() + + @pytest.fixture + def mock_transactions(self): + return [ + {'category': 'groceries', 'amount': -300, 'date': '2024-03-10'}, + {'category': 'groceries', 'amount': -150, 'date': '2024-03-12'}, + {'category': 'dining', 'amount': -200, 'date': '2024-03-14'}, + {'category': 'entertainment', 'amount': -100, 'date': '2024-03-15'}, + ] + + @pytest.fixture + def sample_budget_response(self): + categories = [ + BudgetCategory( + category='groceries', + type='need', + avg_monthly_spent=300, + monthly_budget=280, + recommended_budget=65, + is_essential=True + ), + BudgetCategory( + category='dining', + type='want', + avg_monthly_spent=200, + monthly_budget=150, + recommended_budget=35, + is_essential=False + ), + BudgetCategory( + category='entertainment', + type='want', + avg_monthly_spent=150, + monthly_budget=120, + recommended_budget=28, + is_essential=False + ), + ] + return BudgetResponse( + budget=categories, + generated_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ) + + # Synchronous tests + def test_get_period_boundaries_weekly(self, performance_calc): + ref_date = pd.Timestamp('2024-03-15') + start, end = performance_calc.get_period_boundaries('w', ref_date) + assert start.strftime('%Y-%m-%d') == '2024-03-10' + assert end.strftime('%Y-%m-%d') == '2024-03-16' + + def test_get_period_boundaries_monthly(self, performance_calc): + ref_date = pd.Timestamp('2024-03-15') + start, end = performance_calc.get_period_boundaries('m', ref_date) + assert start.strftime('%Y-%m-%d') == '2024-03-01' + assert end.strftime('%Y-%m-%d') == '2024-03-31' + + def test_get_period_boundaries_yearly(self, performance_calc): + ref_date = pd.Timestamp('2024-03-15') + start, end = performance_calc.get_period_boundaries('y', ref_date) + assert start.strftime('%Y-%m-%d') == '2024-01-01' + assert end.strftime('%Y-%m-%d') == '2024-12-31' + + def test_calculate_actual_spending(self, performance_calc, mock_transactions): + result = performance_calc.calculate_actual_spending(mock_transactions) + assert result['groceries'] == 450 + assert result['dining'] == 200 + assert result['entertainment'] == 100 + + # Async tests with proper mocking + async def test_generate_performance( + self, + performance_calc, + sample_budget_response, + mock_transactions + ): + user_id = 123 + period = 'w' + + # Mock the database function at the EXACT import path used in budgetPerfCalc + with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: + # Configure the mock to return our test data + mock_get.return_value = mock_transactions + + # Call the async function + result = await performance_calc.generate_performance( + user_id, + sample_budget_response, + period + ) + + # Assertions + assert 'categories' in result + assert 'budgetAmounts' in result + assert 'actualAmounts' in result + assert len(result['categories']) == len(sample_budget_response.budget) + + # Verify the mock was called with correct arguments + mock_get.assert_called_once_with(user_id, ANY, ANY) + + async def test_generate_performance_empty_transactions( + self, + performance_calc, + sample_budget_response + ): + user_id = 123 + period = 'w' + + with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: + mock_get.return_value = [] + + result = await performance_calc.generate_performance( + user_id, + sample_budget_response, + period + ) + + # All actual amounts should be 0 + assert all(amount == 0 for amount in result['actualAmounts']) + mock_get.assert_called_once() + + async def test_generate_performance_db_error( + self, + performance_calc, + sample_budget_response + ): + user_id = 123 + period = 'w' + + with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: + mock_get.side_effect = Exception("Database connection failed") + + # The error should propagate + with pytest.raises(Exception, match="Database connection failed"): + await performance_calc.generate_performance( + user_id, + sample_budget_response, + period + ) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/budget_integration.py b/app/server/services/python/analytics/test/budget_integration.py new file mode 100644 index 0000000..56724c3 --- /dev/null +++ b/app/server/services/python/analytics/test/budget_integration.py @@ -0,0 +1,38 @@ +import pytest +from unittest.mock import patch, AsyncMock +from src.logic.budget import generate_budget, generate_budget_performance +from src.models.schemas import BudgetResponse + +class TestBudgetIntegration: + + @pytest.mark.asyncio + async def test_full_budget_flow(self): + user_id = 123 + period = 'w' + + # Mock transactions for generate_budget + mock_transactions = [ + {'amount': 5000, 'category': 'salary', 'date': '2024-01-01'}, + {'amount': -300, 'category': 'groceries', 'date': '2024-01-02'}, + {'amount': -200, 'category': 'dining', 'date': '2024-01-03'}, + {'amount': 5000, 'category': 'salary', 'date': '2024-02-01'}, + {'amount': -250, 'category': 'groceries', 'date': '2024-02-02'}, + ] + + with patch( + 'src.logic.budget.get_user_transactions_with_connection', + return_value=mock_transactions + ): + # Generate budget + budget = generate_budget(period, user_id) + + assert isinstance(budget, BudgetResponse) + assert len(budget.budget) > 0 + + # Generate performance + performance = await generate_budget_performance(user_id, budget, period) + + assert 'categories' in performance + assert 'budgetAmounts' in performance + assert 'actualAmounts' in performance + assert len(performance['categories']) == len(budget.budget) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/cat_classifier.py b/app/server/services/python/analytics/test/cat_classifier.py new file mode 100644 index 0000000..ef7cb27 --- /dev/null +++ b/app/server/services/python/analytics/test/cat_classifier.py @@ -0,0 +1,55 @@ +import pytest +from src.utils.trans_cat_classifier import CategoryClassifier + +class TestCategoryClassifier: + + def test_classify_needs(self, classifier): + assert classifier.classify('rent') == 'need' + assert classifier.classify('groceries') == 'need' + assert classifier.classify('healthcare') == 'need' + assert classifier.classify('utilities') == 'need' + + def test_classify_wants(self, classifier): + assert classifier.classify('dining') == 'want' + assert classifier.classify('entertainment') == 'want' + assert classifier.classify('shopping') == 'want' + assert classifier.classify('coffee') == 'want' + + def test_classify_savings(self, classifier): + assert classifier.classify('savings') == 'savings' + assert classifier.classify('investment') == 'savings' + assert classifier.classify('retirement') == 'savings' + + def test_classify_unknown_defaults_to_want(self, classifier): + assert classifier.classify('unknown_category') == 'want' + assert classifier.classify('random_stuff') == 'want' + + def test_classify_case_insensitive(self, classifier): + assert classifier.classify('RENT') == 'need' + assert classifier.classify('Groceries') == 'need' + assert classifier.classify('DINING') == 'want' + + def test_classify_many(self, classifier): + categories = {'rent', 'dining', 'savings', 'unknown'} + result = classifier.classify_many(categories) + + assert result['rent'] == 'need' + assert result['dining'] == 'want' + assert result['savings'] == 'savings' + assert result['unknown'] == 'want' + + def test_get_categories_by_type(self, classifier): + category_types = { + 'rent': 'need', + 'groceries': 'need', + 'dining': 'want', + 'savings': 'savings' + } + + needs = classifier.get_categories_by_type(category_types, 'need') + wants = classifier.get_categories_by_type(category_types, 'want') + savings = classifier.get_categories_by_type(category_types, 'savings') + + assert needs == {'rent', 'groceries'} + assert wants == {'dining'} + assert savings == {'savings'} \ No newline at end of file diff --git a/app/server/services/python/analytics/test/dependencies.py b/app/server/services/python/analytics/test/dependencies.py new file mode 100644 index 0000000..df09d1d --- /dev/null +++ b/app/server/services/python/analytics/test/dependencies.py @@ -0,0 +1,76 @@ +import pytest +from unittest.mock import patch, MagicMock +import mysql.connector +from src.dependencies import get_db_connection, get_current_user +from fastapi import HTTPException +import os +import jwt +from jwt import InvalidSignatureError + +class TestDatabaseConnection: + + @patch('mysql.connector.connect') + def test_get_db_connection_success(self, mock_connect, mock_env_vars): + mock_connection = MagicMock() + mock_connect.return_value = mock_connection + + result = get_db_connection() + + mock_connect.assert_called_once_with( + host='localhost', + user='test_user', + password='test_password', + database='test_db' + ) + + assert result == mock_connection + + @patch('mysql.connector.connect') + def test_get_db_connection_missing_env_vars(self, mock_connect): + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(HTTPException): + get_db_connection() + + @patch('mysql.connector.connect') + def test_get_db_connection_failure(self, mock_connect): + mock_connect.side_effect = mysql.connector.Error("Connection failed") + with patch.dict(os.environ, {'MYSQL_HOST': 'localhost', 'MYSQL_USER': 'test_user', 'MYSQL_PASSWORD': 'test_password', 'DB_NAME': 'test_db'}, clear=True): + with pytest.raises(mysql.connector.Error): + get_db_connection() + + + +class TestJWTAuthentication: + + @pytest.mark.asyncio + async def test_get_current_user_missing_user_id(self): + + payload = {'email': 'test@example.com'} + token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') + + # Ensure token is string + if isinstance(token, bytes): + token = token.decode('utf-8') + + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): + with pytest.raises(HTTPException) as exc_info: + await get_current_user(authorization=f"Bearer {token}") + + assert exc_info.value.status_code == 401 + assert exc_info.value.detail == "User ID not found in token" + + @pytest.mark.asyncio + async def test_get_current_user_invalid_signature(self): + + payload = {'sub': '123'} + token = jwt.encode(payload, 'wrong_secret', algorithm='HS256') + + if isinstance(token, bytes): + token = token.decode('utf-8') + + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): + with pytest.raises(HTTPException) as exc_info: + await get_current_user(authorization=f"Bearer {token}") + + assert exc_info.value.status_code == 401 + assert "token" in exc_info.value.detail.lower() \ No newline at end of file diff --git a/app/server/services/python/analytics/test/incomeflow.py b/app/server/services/python/analytics/test/incomeflow.py new file mode 100644 index 0000000..18945bf --- /dev/null +++ b/app/server/services/python/analytics/test/incomeflow.py @@ -0,0 +1,131 @@ +# tests/test_service_incomeflow.py +import pytest +import pandas as pd +from src.logic.incomeflow import build_sankey_data + +class TestIncomeflowService: + + def test_build_sankey_data_with_income_and_expenses(self, mock_incomeflow_transactions): + result = build_sankey_data(mock_incomeflow_transactions) + + assert 'nodes' in result + assert 'links' in result + + # Should have total income node + node_names = [node['name'] for node in result['nodes']] + assert 'total income' in node_names + + # Should have all unique categories as nodes + expected_categories = {'salary', 'freelance', 'interest', 'rent', + 'groceries', 'dining', 'entertainment', 'utilities'} + for category in expected_categories: + assert category in node_names + + # Should have links + assert len(result['links']) > 0 + + # Verify link structure + for link in result['links']: + assert 'source' in link + assert 'target' in link + assert 'value' in link + assert isinstance(link['value'], int) + + def test_build_sankey_data_with_overflow(self): + transactions = [ + {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, + {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, + {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, + {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'}, + ] + + result = build_sankey_data(transactions) + + # Should have 'unspent' node + node_names = [node['name'] for node in result['nodes']] + assert 'unspent' in node_names + + # Should have link to unspent + unspent_index = node_names.index('unspent') + total_income_index = node_names.index('total income') + + # Find link from total income to unspent + unspent_links = [link for link in result['links'] + if link['source'] == total_income_index + and link['target'] == unspent_index] + assert len(unspent_links) == 1 + assert unspent_links[0]['value'] == 4000 # 6000 - 2000 + + def test_build_sankey_data_with_overspending(self): + transactions = [ + {'amount': 3000, 'category': 'salary', 'date': '2024-03-01'}, + {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, + {'amount': -1000, 'category': 'groceries', 'date': '2024-03-04'}, + {'amount': -800, 'category': 'dining', 'date': '2024-03-05'}, + ] + + result = build_sankey_data(transactions) + + # Should have 'overspent' and 'savings' nodes + node_names = [node['name'] for node in result['nodes']] + assert 'overspent' in node_names + assert 'savings' in node_names + + # Should have link from savings to overspent + savings_index = node_names.index('savings') + overspent_index = node_names.index('overspent') + + overspent_links = [link for link in result['links'] + if link['source'] == savings_index + and link['target'] == overspent_index] + assert len(overspent_links) == 1 + assert overspent_links[0]['value'] == 300 # 3300 - 3000 + + def test_build_sankey_data_empty_transactions(self): + """Test sankey data with no transactions.""" + result = build_sankey_data([]) + + assert result['nodes'] == [{'name': 'total income'}, {'name': 'no data'}] + assert result['links'] == [] + + def test_build_sankey_data_only_income(self): + transactions = [ + {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, + {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, + ] + + result = build_sankey_data(transactions) + + node_names = [node['name'] for node in result['nodes']] + assert 'unspent' in node_names + + # All income should flow to unspent + total_income_index = node_names.index('total income') + unspent_index = node_names.index('unspent') + + unspent_links = [link for link in result['links'] + if link['source'] == total_income_index + and link['target'] == unspent_index] + assert unspent_links[0]['value'] == 6000 + + def test_build_sankey_data_only_expenses(self): + transactions = [ + {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, + {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'}, + ] + + result = build_sankey_data(transactions) + + node_names = [node['name'] for node in result['nodes']] + assert 'overspent' in node_names + assert 'savings' in node_names + + # Expenses should come from savings + savings_index = node_names.index('savings') + overspent_index = node_names.index('overspent') + rent_index = node_names.index('rent') + groceries_index = node_names.index('groceries') + + # Links from savings to overspent and to categories + savings_links = [link for link in result['links'] if link['source'] == savings_index] + assert len(savings_links) == 1 # savings into overspent \ No newline at end of file diff --git a/app/server/services/python/analytics/test/periodCalc.py b/app/server/services/python/analytics/test/periodCalc.py new file mode 100644 index 0000000..8e272e9 --- /dev/null +++ b/app/server/services/python/analytics/test/periodCalc.py @@ -0,0 +1,146 @@ +import pytest +import pandas as pd +from src.utils.dates import period_calc + +class TestPeriodCalc: + + def test_period_calc_weekly(self): + end_date = pd.Timestamp('2024-03-15') + + start_date, freq, date_format, period_name = period_calc('w', end_date) + + # Check start date is 7 days before end date + expected_start = end_date - pd.Timedelta(days=7) + assert start_date == expected_start + assert start_date.strftime('%Y-%m-%d') == '2024-03-08' + + # Check other return values + assert freq == 'D' + assert date_format == '%Y-%m-%d' + assert period_name == 'Weekly' + + def test_period_calc_monthly(self): + end_date = pd.Timestamp('2024-03-15') + + start_date, freq, date_format, period_name = period_calc('m', end_date) + + # Check start date is 30 days before end date + expected_start = end_date - pd.Timedelta(days=30) + assert start_date == expected_start + assert start_date.strftime('%Y-%m-%d') == '2024-02-14' # March 15 - 30 days = Feb 14 + + # Check other return values + assert freq == 'D' + assert date_format == '%Y-%m-%d' + assert period_name == 'Monthly' + + def test_period_calc_yearly(self): + end_date = pd.Timestamp('2024-03-15') + + start_date, freq, date_format, period_name = period_calc('y', end_date) + + # Check start date is 365 days before end date + expected_start = end_date - pd.Timedelta(days=365) + assert start_date == expected_start + assert start_date.strftime('%Y-%m-%d') == '2023-03-16' # 2024 is leap year + + # Check other return values + assert freq == 'M' + assert date_format == '%Y-%m' + assert period_name == 'Yearly' + + def test_period_calc_with_different_end_dates(self): + test_cases = [ + # (end_date, expected_weekly_start, expected_monthly_start, expected_yearly_start) + ('2024-01-01', '2023-12-25', '2023-12-02', '2023-01-01'), + ('2024-06-15', '2024-06-08', '2024-05-16', '2023-06-16'), + ('2024-12-31', '2024-12-24', '2024-12-01', '2024-01-01'), + ] + + for end_str, expected_weekly, expected_monthly, expected_yearly in test_cases: + end_date = pd.Timestamp(end_str) + + # Test weekly + start, freq, fmt, name = period_calc('w', end_date) + assert start.strftime('%Y-%m-%d') == expected_weekly + + # Test monthly + start, freq, fmt, name = period_calc('m', end_date) + assert start.strftime('%Y-%m-%d') == expected_monthly + + # Test yearly + start, freq, fmt, name = period_calc('y', end_date) + assert start.strftime('%Y-%m-%d') == expected_yearly + + def test_period_calc_edge_cases(self): + + end_date = pd.Timestamp('2024-02-29') # Leap year + start, _, _, _ = period_calc('m', end_date) + assert start.strftime('%Y-%m-%d') == '2024-01-30' # Feb 29 - 30 days = Jan 30 + + end_date = pd.Timestamp('2024-12-31') + start, _, _, _ = period_calc('y', end_date) + assert start.strftime('%Y-%m-%d') == '2024-01-01' + + end_date = pd.Timestamp('2024-01-01') + start, _, _, _ = period_calc('m', end_date) + assert start.strftime('%Y-%m-%d') == '2023-12-02' # Jan 1 - 30 days = Dec 2 + + def test_period_calc_invalid_period(self): + end_date = pd.Timestamp('2024-03-15') + + # Invalid period should go to else branch (yearly) + start, freq, date_format, period_name = period_calc('invalid', end_date) + + expected_start = end_date - pd.Timedelta(days=365) + assert start == expected_start + assert freq == 'M' + assert date_format == '%Y-%m' + assert period_name == 'Yearly' + + def test_period_calc_with_time_component(self): + end_date = pd.Timestamp('2024-03-15 14:30:45') + + start, _, _, _ = period_calc('w', end_date) + + # Time component should be preserved in the calculation + expected_start = end_date - pd.Timedelta(days=7) + assert start == expected_start + assert start.hour == 14 + assert start.minute == 30 + assert start.second == 45 + + def test_period_calc_returns_tuple(self): + end_date = pd.Timestamp('2024-03-15') + result = period_calc('w', end_date) + + assert isinstance(result, tuple) + assert len(result) == 4 + + start, freq, date_format, period_name = result + assert isinstance(start, pd.Timestamp) + assert isinstance(freq, str) + assert isinstance(date_format, str) + assert isinstance(period_name, str) + + @pytest.mark.parametrize("period,expected_freq,expected_format,expected_name", [ + ('w', 'D', '%Y-%m-%d', 'Weekly'), + ('m', 'D', '%Y-%m-%d', 'Monthly'), + ('y', 'M', '%Y-%m', 'Yearly'), + ]) + def test_period_calc_parametrized(self, period, expected_freq, expected_format, expected_name): + end_date = pd.Timestamp('2024-03-15') + + start, freq, date_format, period_name = period_calc(period, end_date) + + assert freq == expected_freq + assert date_format == expected_format + assert period_name == expected_name + + # Verify start date calculation + if period == 'w': + assert start == end_date - pd.Timedelta(days=7) + elif period == 'm': + assert start == end_date - pd.Timedelta(days=30) + else: + assert start == end_date - pd.Timedelta(days=365) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/savings.py b/app/server/services/python/analytics/test/savings.py new file mode 100644 index 0000000..02d7216 --- /dev/null +++ b/app/server/services/python/analytics/test/savings.py @@ -0,0 +1,99 @@ +import pytest +import pandas as pd +from datetime import datetime +from unittest.mock import patch, MagicMock +from src.logic.savings import calculate_savings_over_time + +class TestSavings: + + def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_savings_transactions): + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=7) + + result = calculate_savings_over_time( + mock_savings_accounts, + mock_savings_transactions, + start_date, + end_date, + 'w', + 'Weekly' + ) + + assert 'labels' in result + assert 'datasets' in result + assert len(result['labels']) == 8 # 7 days + 1? Check your implementation + assert result['datasets'][0]['label'] == 'Weekly Savings' + + # Verify data is numeric + for value in result['datasets'][0]['data']: + assert isinstance(value, (int, float)) + + def test_calculate_savings_over_time_monthly(self, mock_savings_accounts, mock_savings_transactions): + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=30) + + result = calculate_savings_over_time( + mock_savings_accounts, + mock_savings_transactions, + start_date, + end_date, + 'm', + 'Monthly' + ) + + assert result['datasets'][0]['label'] == 'Monthly Savings' + assert len(result['labels']) > 0 + + def test_calculate_savings_over_time_yearly(self, mock_savings_accounts, mock_savings_transactions): + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=365) + + result = calculate_savings_over_time( + mock_savings_accounts, + mock_savings_transactions, + start_date, + end_date, + 'y', + 'Yearly' + ) + + assert result['datasets'][0]['label'] == 'Yearly Savings' + # Yearly should have monthly labels + assert all(len(label) == 7 for label in result['labels']) # YYYY-MM format + + def test_calculate_savings_over_time_empty_transactions(self, mock_savings_accounts): + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=7) + + result = calculate_savings_over_time( + mock_savings_accounts, + [], # Empty transactions + start_date, + end_date, + 'w', + 'Weekly' + ) + + # Should still return structure with zero data + assert 'labels' in result + assert 'datasets' in result + # All savings values should be the initial balances + expected_initial = sum(acc['balance'] for acc in mock_savings_accounts) + assert all(value == expected_initial for value in result['datasets'][0]['data']) + + def test_calculate_savings_over_time_no_accounts(self): + end_date = pd.Timestamp('2024-03-15') + start_date = end_date - pd.Timedelta(days=7) + + result = calculate_savings_over_time( + [], # Empty accounts + [], + start_date, + end_date, + 'w', + 'Weekly' + ) + + # Should return empty structure + assert result['labels'] == [] + assert result['datasets'][0]['data'] == [] \ No newline at end of file diff --git a/app/server/services/python/market/Dockerfile.integration.test b/app/server/services/python/market/Dockerfile.integration.test new file mode 100644 index 0000000..d0d3e0e --- /dev/null +++ b/app/server/services/python/market/Dockerfile.integration.test @@ -0,0 +1,13 @@ +FROM measureonecodetwice/finus-market:local + +WORKDIR /app + +COPY integration ./integration +COPY requirements.txt . +COPY requirements-test.txt . + +RUN pip install -r requirements-test.txt + +ENV CI=true + +CMD ["python", "-m", "pytest", "integration/", "-v"] diff --git a/app/server/services/python/market/Dockerfile.mutation.test b/app/server/services/python/market/Dockerfile.mutation.test new file mode 100644 index 0000000..6f01ab3 --- /dev/null +++ b/app/server/services/python/market/Dockerfile.mutation.test @@ -0,0 +1,21 @@ +FROM measureonecodetwice/finus-market:local + +WORKDIR /app + +COPY src ./src +COPY test ./test +COPY requirements.txt . +COPY requirements-test.txt . +COPY pyproject.toml . + +RUN pip install -r requirements-test.txt + +RUN cp -r src market_src +RUN cp -r test mutation_test +RUN find mutation_test -type f -name "*.py" -exec perl -0pi -e "s/\\bfrom src import\\b/from market_src import/g; s/\\bimport src\\./import market_src./g; s/\\bsrc\\./market_src./g" {} + +RUN rm -rf src test + +ENV PYTHONPATH=/app +ENV CI=true + +CMD ["mutmut", "run"] diff --git a/app/server/services/python/market/Dockerfile.test b/app/server/services/python/market/Dockerfile.test new file mode 100644 index 0000000..81be651 --- /dev/null +++ b/app/server/services/python/market/Dockerfile.test @@ -0,0 +1,15 @@ +FROM measureonecodetwice/finus-market:local + +WORKDIR /app + +COPY test ./test +COPY requirements.txt . +COPY requirements-test.txt . + +RUN pip install -r requirements-test.txt + +ENV PYTHONPATH=/app +ENV JWT_SECRET=test_secret_key_12345 +ENV CI=true + +CMD ["python", "-m", "pytest", "test/", "-v", "--cov=src"] diff --git a/app/server/services/python/market/integration/test_market_integration.py b/app/server/services/python/market/integration/test_market_integration.py new file mode 100644 index 0000000..a50b45c --- /dev/null +++ b/app/server/services/python/market/integration/test_market_integration.py @@ -0,0 +1,181 @@ +import os +import time + +import httpx +import pytest + + +MARKET_SERVICE_ADDR = os.getenv("MARKET_SERVICE_ADDR", "http://market:8000") +READY_TIMEOUT_SECONDS = float(os.getenv("MARKET_READY_TIMEOUT_SECONDS", "60")) + + +def wait_for_market_service(base_url: str, timeout_seconds: float) -> None: + deadline = time.monotonic() + timeout_seconds + last_error = "market service never became reachable" + + while time.monotonic() < deadline: + try: + response = httpx.get(f"{base_url}/health", timeout=5.0) + if response.status_code == 200 and response.json() == "ok": + return + last_error = f"unexpected health response: {response.status_code} {response.text}" + except httpx.HTTPError as exc: + last_error = str(exc) + + time.sleep(1) + + pytest.fail(f"Market service was not ready within {timeout_seconds}s: {last_error}") + + +@pytest.fixture(scope="session") +def market_client() -> httpx.Client: + wait_for_market_service(MARKET_SERVICE_ADDR, READY_TIMEOUT_SECONDS) + + with httpx.Client(base_url=MARKET_SERVICE_ADDR, timeout=15.0) as client: + yield client + + +def test_market_health_endpoint(market_client: httpx.Client) -> None: + response = market_client.get("/health") + + assert response.status_code == 200 + assert response.json() == "ok" + + +def test_search_endpoint_returns_enriched_market_results( + market_client: httpx.Client, +) -> None: + response = market_client.get("/markets/search", params={"q": "", "limit": 2}) + + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload, list) + assert 0 < len(payload) <= 2 + + result = payload[0] + assert result["symbol"] + assert result["displaySymbol"] + assert result["name"] + assert result["type"] in {"stock", "forex"} + assert isinstance(result["price"], (int, float)) + assert isinstance(result["timestamp"], int) + assert result["source"] == "yahoo" + + +def test_search_endpoint_respects_limit_param(market_client: httpx.Client) -> None: + response = market_client.get("/markets/search", params={"q": "", "limit": 1}) + + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload, list) + assert len(payload) == 1 + + +def test_search_endpoint_can_return_forex_results_for_forex_query( + market_client: httpx.Client, +) -> None: + response = market_client.get("/markets/search", params={"q": "EUR", "limit": 8}) + + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload, list) + assert any(item["type"] == "forex" for item in payload) + + +def test_search_endpoint_rejects_limit_above_maximum( + market_client: httpx.Client, +) -> None: + response = market_client.get("/markets/search", params={"q": "", "limit": 21}) + + assert response.status_code == 422 + + +def test_search_endpoint_rejects_limit_below_minimum( + market_client: httpx.Client, +) -> None: + response = market_client.get("/markets/search", params={"q": "", "limit": 0}) + + assert response.status_code == 422 + + +def test_quote_endpoint_returns_live_snapshot(market_client: httpx.Client) -> None: + response = market_client.get("/markets/quote", params={"symbol": "AAPL"}) + + assert response.status_code == 200 + payload = response.json() + assert payload["symbol"] == "AAPL" + assert isinstance(payload["price"], (int, float)) + assert isinstance(payload["timestamp"], int) + assert payload["source"] == "yahoo" + + +def test_quote_endpoint_requires_symbol(market_client: httpx.Client) -> None: + response = market_client.get("/markets/quote") + + assert response.status_code == 422 + + +def test_history_endpoint_returns_points_for_supported_period( + market_client: httpx.Client, +) -> None: + response = market_client.get( + "/markets/history", + params={"symbol": "AAPL", "period": "1mo", "interval": "1d"}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["symbol"] == "AAPL" + assert payload["period"] == "1mo" + assert payload["interval"] == "1d" + assert payload["source"] == "yahoo" + assert isinstance(payload["points"], list) + assert payload["points"] + assert isinstance(payload["points"][0]["timestamp"], int) + assert isinstance(payload["points"][0]["price"], (int, float)) + + +def test_history_endpoint_points_are_in_chronological_order( + market_client: httpx.Client, +) -> None: + response = market_client.get( + "/markets/history", + params={"symbol": "AAPL", "period": "1mo", "interval": "1d"}, + ) + + assert response.status_code == 200 + timestamps = [point["timestamp"] for point in response.json()["points"]] + assert timestamps == sorted(timestamps) + + +def test_history_endpoint_requires_symbol(market_client: httpx.Client) -> None: + response = market_client.get( + "/markets/history", + params={"period": "1mo", "interval": "1d"}, + ) + + assert response.status_code == 422 + + +def test_history_endpoint_rejects_unsupported_period( + market_client: httpx.Client, +) -> None: + response = market_client.get( + "/markets/history", + params={"symbol": "AAPL", "period": "bad", "interval": "1d"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "Unsupported period." + + +def test_history_endpoint_rejects_unsupported_interval( + market_client: httpx.Client, +) -> None: + response = market_client.get( + "/markets/history", + params={"symbol": "AAPL", "period": "1mo", "interval": "bad"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "Unsupported interval." diff --git a/app/server/services/python/market/pyproject.toml b/app/server/services/python/market/pyproject.toml new file mode 100644 index 0000000..34fe554 --- /dev/null +++ b/app/server/services/python/market/pyproject.toml @@ -0,0 +1,8 @@ +[tool.mutmut] +paths_to_mutate = ["market_src/"] +pytest_add_cli_args_test_selection = ["mutation_test/"] +pytest_add_cli_args = ["-q"] +mutate_only_covered_lines = true +do_not_mutate = ["market_src/__init__.py"] +also_copy = ["mutation_test/"] +debug = true diff --git a/app/server/services/python/market/requirements-test.txt b/app/server/services/python/market/requirements-test.txt new file mode 100644 index 0000000..9924071 --- /dev/null +++ b/app/server/services/python/market/requirements-test.txt @@ -0,0 +1,5 @@ +-r requirements.txt +pytest>=7.0.0 +pytest-cov>=4.1.0 +httpx>=0.27.0,<0.28.0 +mutmut>=3.2.3 diff --git a/app/server/services/python/market/src/__init__.py b/app/server/services/python/market/src/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/server/services/python/market/src/__init__.py @@ -0,0 +1 @@ + diff --git a/app/server/services/python/market/src/config.py b/app/server/services/python/market/src/config.py new file mode 100644 index 0000000..d57f798 --- /dev/null +++ b/app/server/services/python/market/src/config.py @@ -0,0 +1,124 @@ +YAHOO_SEARCH_URL = "https://query1.finance.yahoo.com/v1/finance/search" +YAHOO_CHART_URL = "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}" +YAHOO_SYMBOL_SAFE_CHARS = "=^.-" +SUPPORTED_PERIODS = {"1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y"} +SUPPORTED_INTERVALS = {"5m", "15m", "1d", "1wk", "1mo"} +DEFAULT_SEARCH_LIMIT = 8 +REQUEST_HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/137.0.0.0 Safari/537.36" + ) +} + +FEATURED_STOCKS = [ + { + "symbol": "AAPL", + "displaySymbol": "AAPL", + "name": "Apple Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "MSFT", + "displaySymbol": "MSFT", + "name": "Microsoft Corporation", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "NVDA", + "displaySymbol": "NVDA", + "name": "NVIDIA Corporation", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "AMZN", + "displaySymbol": "AMZN", + "name": "Amazon.com, Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "GOOGL", + "displaySymbol": "GOOGL", + "name": "Alphabet Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "TSLA", + "displaySymbol": "TSLA", + "name": "Tesla, Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, +] + +FOREX_CATALOG = [ + { + "symbol": "EURUSD=X", + "displaySymbol": "EUR/USD", + "name": "Euro to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "GBPUSD=X", + "displaySymbol": "GBP/USD", + "name": "British Pound to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "USDJPY=X", + "displaySymbol": "USD/JPY", + "name": "US Dollar to Japanese Yen", + "type": "forex", + "currency": "JPY", + }, + { + "symbol": "USDCAD=X", + "displaySymbol": "USD/CAD", + "name": "US Dollar to Canadian Dollar", + "type": "forex", + "currency": "CAD", + }, + { + "symbol": "AUDUSD=X", + "displaySymbol": "AUD/USD", + "name": "Australian Dollar to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "NZDUSD=X", + "displaySymbol": "NZD/USD", + "name": "New Zealand Dollar to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "USDCHF=X", + "displaySymbol": "USD/CHF", + "name": "US Dollar to Swiss Franc", + "type": "forex", + "currency": "CHF", + }, + { + "symbol": "EURGBP=X", + "displaySymbol": "EUR/GBP", + "name": "Euro to British Pound", + "type": "forex", + "currency": "GBP", + }, +] + diff --git a/app/server/services/python/market/src/main.py b/app/server/services/python/market/src/main.py index e967fe8..2a4c945 100644 --- a/app/server/services/python/market/src/main.py +++ b/app/server/services/python/market/src/main.py @@ -1,14 +1,72 @@ -from fastapi import FastAPI, HTTPException -import uvicorn import os +import uvicorn +from fastapi import FastAPI, HTTPException, Query + +from .config import DEFAULT_SEARCH_LIMIT, SUPPORTED_INTERVALS, SUPPORTED_PERIODS +from .service import build_history, enrich_search_results, fetch_quote_snapshot +from .yahoo_client import ( + search_forex_catalog, + search_stock_catalog, + yahoo_search, +) + app = FastAPI() -@app.get('/health') + +@app.get("/health") def test_endpoint(): - return 'ok' + return "ok" + + +@app.get("/markets/search") +def search_markets( + q: str = Query(default="", min_length=0), + limit: int = Query(default=DEFAULT_SEARCH_LIMIT, ge=1, le=20), +): + forex_matches = search_forex_catalog(q, limit) + stock_matches = search_stock_catalog(q, limit) + yahoo_matches = yahoo_search(q, limit) + + combined = [] + seen_symbols = set() + for item in [*stock_matches, *forex_matches, *yahoo_matches]: + if item["symbol"] in seen_symbols: + continue + seen_symbols.add(item["symbol"]) + combined.append(item) + if len(combined) >= limit: + break + + return enrich_search_results(combined) + + +@app.get("/markets/quote") +def get_market_quote(symbol: str = Query(min_length=1)): + return fetch_quote_snapshot(symbol) + + +@app.get("/markets/history") +def get_market_history( + symbol: str = Query(min_length=1), + period: str = Query(default="6mo"), + interval: str = Query(default="1d"), +): + if period not in SUPPORTED_PERIODS: + raise HTTPException(status_code=400, detail="Unsupported period.") + if interval not in SUPPORTED_INTERVALS: + raise HTTPException(status_code=400, detail="Unsupported interval.") + + points, source = build_history(symbol, period, interval) + return { + "symbol": symbol, + "period": period, + "interval": interval, + "source": source, + "points": points, + } + if __name__ == "__main__": - port = int(os.getenv("PORT", 8000)) uvicorn.run(app, port=port) diff --git a/app/server/services/python/market/src/service.py b/app/server/services/python/market/src/service.py new file mode 100644 index 0000000..ab964f7 --- /dev/null +++ b/app/server/services/python/market/src/service.py @@ -0,0 +1,58 @@ +from fastapi import HTTPException + +from .utils import format_change, to_float +from .yahoo_client import extract_chart_points, fetch_yahoo_chart + + +def fetch_quote_snapshot(symbol: str) -> dict: + chart_result = fetch_yahoo_chart(symbol, "1mo", "1d") + points = extract_chart_points(chart_result) + meta = chart_result.get("meta") or {} + + latest_point = points[-1] + latest_price = to_float(meta.get("regularMarketPrice")) + if latest_price is None: + latest_price = latest_point["price"] + + previous_close = to_float(meta.get("chartPreviousClose")) + if previous_close is None: + previous_close = to_float(meta.get("previousClose")) + if previous_close is None and len(points) > 1: + previous_close = points[-2]["price"] + + change, change_percent = format_change(latest_price, previous_close) + + return { + "symbol": symbol, + "price": latest_price, + "change": change, + "changePercent": change_percent, + "timestamp": latest_point["timestamp"], + "source": "yahoo", + } + + +def build_history(symbol: str, period: str, interval: str) -> tuple[list[dict], str]: + chart_result = fetch_yahoo_chart(symbol, period, interval) + return extract_chart_points(chart_result), "yahoo" + + +def enrich_search_results(items: list[dict]) -> list[dict]: + enriched = [] + for item in items: + try: + snapshot = fetch_quote_snapshot(item["symbol"]) + enriched.append( + { + **item, + "price": snapshot["price"], + "change": snapshot["change"], + "changePercent": snapshot["changePercent"], + "timestamp": snapshot["timestamp"], + "source": snapshot["source"], + } + ) + except HTTPException: + continue + + return enriched diff --git a/app/server/services/python/market/src/utils.py b/app/server/services/python/market/src/utils.py new file mode 100644 index 0000000..9b060f5 --- /dev/null +++ b/app/server/services/python/market/src/utils.py @@ -0,0 +1,46 @@ +import math +from datetime import datetime, timezone + + +def is_number(value) -> bool: + return isinstance(value, (int, float)) and math.isfinite(value) + + +def to_float(value) -> float | None: + if is_number(value): + return round(float(value), 6) + return None + + +def to_timestamp(value) -> int | None: + if value is None: + return None + + if hasattr(value, "to_pydatetime"): + value = value.to_pydatetime() + + if isinstance(value, datetime): + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + return int(value.timestamp()) + + return None + + +def format_change( + current: float | None, previous: float | None +) -> tuple[float | None, float | None]: + if current is None or previous is None: + return None, None + + change = round(current - previous, 6) + if previous == 0: + return change, None + + change_percent = round((change / previous) * 100, 4) + return change, change_percent + + +def normalize_query(value: str) -> str: + return "".join(char.lower() for char in value if char.isalnum()) + diff --git a/app/server/services/python/market/src/yahoo_client.py b/app/server/services/python/market/src/yahoo_client.py new file mode 100644 index 0000000..a0e612a --- /dev/null +++ b/app/server/services/python/market/src/yahoo_client.py @@ -0,0 +1,171 @@ +from typing import Literal +from urllib.parse import quote + +import requests +from fastapi import HTTPException + +from .config import ( + FEATURED_STOCKS, + FOREX_CATALOG, + REQUEST_HEADERS, + YAHOO_CHART_URL, + YAHOO_SEARCH_URL, + YAHOO_SYMBOL_SAFE_CHARS, +) +from .utils import normalize_query, to_float + + +def fetch_yahoo_chart(symbol: str, period: str, interval: str) -> dict: + try: + response = requests.get( + YAHOO_CHART_URL.format(symbol=quote(symbol, safe=YAHOO_SYMBOL_SAFE_CHARS)), + params={ + "range": period, + "interval": interval, + "includePrePost": "false", + "events": "div,splits", + }, + headers=REQUEST_HEADERS, + timeout=8, + ) + response.raise_for_status() + payload = response.json() + except (requests.RequestException, ValueError) as exc: + raise HTTPException( + status_code=502, + detail="Unable to reach Yahoo market data.", + ) from exc + + chart = payload.get("chart") or {} + if chart.get("error"): + description = chart["error"].get("description") + raise HTTPException( + status_code=404, + detail=description or "Yahoo market data not found for symbol.", + ) + + results = chart.get("result") or [] + if not results: + raise HTTPException( + status_code=404, + detail="Yahoo market data not found for symbol.", + ) + + return results[0] + + +def extract_chart_points(chart_result: dict) -> list[dict]: + timestamps = chart_result.get("timestamp") or [] + indicators = chart_result.get("indicators") or {} + quotes = indicators.get("quote") or [] + if not quotes: + raise HTTPException( + status_code=404, + detail="Historical data not found for symbol.", + ) + + first_quote = quotes[0] or {} + closes = first_quote.get("close") or [] + points = [] + for timestamp, close in zip(timestamps, closes): + close_price = to_float(close) + if close_price is None: + continue + + points.append({"timestamp": int(timestamp), "price": close_price}) + + if not points: + raise HTTPException( + status_code=404, + detail="Historical data not found for symbol.", + ) + + return points + + +def search_forex_catalog(query: str, limit: int) -> list[dict]: + normalized_query = normalize_query(query) + if not normalized_query: + return FOREX_CATALOG[:limit] + + matches = [] + for item in FOREX_CATALOG: + haystack = normalize_query( + f"{item['symbol']} {item['displaySymbol']} {item['name']}" + ) + if normalized_query in haystack: + matches.append(item) + + return matches[:limit] + + +def search_stock_catalog(query: str, limit: int) -> list[dict]: + normalized_query = normalize_query(query) + if not normalized_query: + return FEATURED_STOCKS[:limit] + + matches = [] + for item in FEATURED_STOCKS: + haystack = normalize_query( + f"{item['symbol']} {item['displaySymbol']} {item['name']}" + ) + if normalized_query in haystack: + matches.append(item) + + return matches[:limit] + + +def yahoo_search(query: str, limit: int) -> list[dict]: + try: + response = requests.get( + YAHOO_SEARCH_URL, + params={"q": query, "quotesCount": limit * 2, "newsCount": 0}, + headers=REQUEST_HEADERS, + timeout=6, + ) + response.raise_for_status() + payload = response.json() + except (requests.RequestException, ValueError): + return [] + + results = [] + for item in payload.get("quotes") or []: + quote_type = item.get("quoteType") + symbol = item.get("symbol") + if not symbol: + continue + + if quote_type == "EQUITY": + instrument_type: Literal["stock", "forex"] = "stock" + elif quote_type == "CURRENCY": + instrument_type = "forex" + else: + continue + + display_symbol = symbol if instrument_type == "stock" else symbol.replace("=X", "") + name = item.get("longname") or item.get("shortname") or symbol + currency = item.get("currency") or "USD" + + results.append( + { + "symbol": symbol, + "displaySymbol": display_symbol, + "name": name, + "type": instrument_type, + "currency": currency, + "exchange": item.get("exchange"), + } + ) + + unique_results = [] + seen_symbols = set() + for result in results: + if result["symbol"] in seen_symbols: + continue + seen_symbols.add(result["symbol"]) + unique_results.append(result) + if len(unique_results) >= limit: + break + + return unique_results + diff --git a/app/server/services/python/market/test/__init__.py b/app/server/services/python/market/test/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/server/services/python/market/test/__init__.py @@ -0,0 +1 @@ + diff --git a/app/server/services/python/market/test/test_config.py b/app/server/services/python/market/test/test_config.py new file mode 100644 index 0000000..bde78e1 --- /dev/null +++ b/app/server/services/python/market/test/test_config.py @@ -0,0 +1,149 @@ +from src import config + + +def test_market_config_core_constants(): + assert ( + config.YAHOO_SEARCH_URL + == "https://query1.finance.yahoo.com/v1/finance/search" + ) + assert ( + config.YAHOO_CHART_URL + == "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}" + ) + assert config.YAHOO_SYMBOL_SAFE_CHARS == "=^.-" + assert config.DEFAULT_SEARCH_LIMIT == 8 + assert config.SUPPORTED_PERIODS == { + "1d", + "5d", + "1mo", + "3mo", + "6mo", + "1y", + "2y", + "5y", + } + assert config.SUPPORTED_INTERVALS == {"5m", "15m", "1d", "1wk", "1mo"} + + +def test_market_config_request_headers(): + assert config.REQUEST_HEADERS == { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/137.0.0.0 Safari/537.36" + ) + } + + +def test_market_config_featured_stocks_catalog_contents(): + assert config.FEATURED_STOCKS == [ + { + "symbol": "AAPL", + "displaySymbol": "AAPL", + "name": "Apple Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "MSFT", + "displaySymbol": "MSFT", + "name": "Microsoft Corporation", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "NVDA", + "displaySymbol": "NVDA", + "name": "NVIDIA Corporation", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "AMZN", + "displaySymbol": "AMZN", + "name": "Amazon.com, Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "GOOGL", + "displaySymbol": "GOOGL", + "name": "Alphabet Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "TSLA", + "displaySymbol": "TSLA", + "name": "Tesla, Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + ] + + +def test_market_config_forex_catalog_contents(): + assert config.FOREX_CATALOG == [ + { + "symbol": "EURUSD=X", + "displaySymbol": "EUR/USD", + "name": "Euro to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "GBPUSD=X", + "displaySymbol": "GBP/USD", + "name": "British Pound to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "USDJPY=X", + "displaySymbol": "USD/JPY", + "name": "US Dollar to Japanese Yen", + "type": "forex", + "currency": "JPY", + }, + { + "symbol": "USDCAD=X", + "displaySymbol": "USD/CAD", + "name": "US Dollar to Canadian Dollar", + "type": "forex", + "currency": "CAD", + }, + { + "symbol": "AUDUSD=X", + "displaySymbol": "AUD/USD", + "name": "Australian Dollar to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "NZDUSD=X", + "displaySymbol": "NZD/USD", + "name": "New Zealand Dollar to US Dollar", + "type": "forex", + "currency": "USD", + }, + { + "symbol": "USDCHF=X", + "displaySymbol": "USD/CHF", + "name": "US Dollar to Swiss Franc", + "type": "forex", + "currency": "CHF", + }, + { + "symbol": "EURGBP=X", + "displaySymbol": "EUR/GBP", + "name": "Euro to British Pound", + "type": "forex", + "currency": "GBP", + }, + ] diff --git a/app/server/services/python/market/test/test_main.py b/app/server/services/python/market/test/test_main.py new file mode 100644 index 0000000..e5bf976 --- /dev/null +++ b/app/server/services/python/market/test/test_main.py @@ -0,0 +1,136 @@ +import os +import runpy +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +import src.main + + +client = TestClient(src.main.app) + + +def test_health_endpoint(): + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == "ok" + + +def test_search_route_combines_sources_and_enriches_results(): + with patch("src.main.search_forex_catalog", return_value=[{"symbol": "EURUSD=X"}]), patch( + "src.main.search_stock_catalog", return_value=[{"symbol": "AAPL"}] + ), patch("src.main.yahoo_search", return_value=[{"symbol": "AAPL"}, {"symbol": "MSFT"}]), patch( + "src.main.enrich_search_results", + return_value=[{"symbol": "AAPL"}, {"symbol": "EURUSD=X"}, {"symbol": "MSFT"}], + ) as enrich_mock: + response = client.get("/markets/search", params={"q": "apple", "limit": 5}) + + assert response.status_code == 200 + assert response.json() == [{"symbol": "AAPL"}, {"symbol": "EURUSD=X"}, {"symbol": "MSFT"}] + enrich_mock.assert_called_once_with( + [{"symbol": "AAPL"}, {"symbol": "EURUSD=X"}, {"symbol": "MSFT"}] + ) + + +def test_search_route_stops_combining_once_limit_is_reached(): + with patch( + "src.main.search_forex_catalog", + return_value=[{"symbol": "EURUSD=X"}, {"symbol": "GBPUSD=X"}], + ), patch( + "src.main.search_stock_catalog", + return_value=[{"symbol": "AAPL"}, {"symbol": "MSFT"}], + ), patch( + "src.main.yahoo_search", + return_value=[{"symbol": "TSLA"}], + ), patch( + "src.main.enrich_search_results", + side_effect=lambda items: items, + ) as enrich_mock: + response = client.get("/markets/search", params={"q": "a", "limit": 2}) + + assert response.status_code == 200 + assert response.json() == [{"symbol": "AAPL"}, {"symbol": "MSFT"}] + enrich_mock.assert_called_once_with([{"symbol": "AAPL"}, {"symbol": "MSFT"}]) + + +def test_search_route_uses_default_query_and_limit(): + with patch("src.main.search_forex_catalog", return_value=[]) as forex_mock, patch( + "src.main.search_stock_catalog", return_value=[] + ) as stock_mock, patch("src.main.yahoo_search", return_value=[]) as yahoo_mock, patch( + "src.main.enrich_search_results", + return_value=[], + ) as enrich_mock: + response = client.get("/markets/search") + + assert response.status_code == 200 + assert response.json() == [] + forex_mock.assert_called_once_with("", src.main.DEFAULT_SEARCH_LIMIT) + stock_mock.assert_called_once_with("", src.main.DEFAULT_SEARCH_LIMIT) + yahoo_mock.assert_called_once_with("", src.main.DEFAULT_SEARCH_LIMIT) + enrich_mock.assert_called_once_with([]) + + +def test_quote_route_delegates_to_service(): + with patch("src.main.fetch_quote_snapshot", return_value={"symbol": "AAPL"}) as quote_mock: + response = client.get("/markets/quote", params={"symbol": "AAPL"}) + + assert response.status_code == 200 + assert response.json() == {"symbol": "AAPL"} + quote_mock.assert_called_once_with("AAPL") + + +def test_history_route_returns_payload_and_validation_errors(): + with patch( + "src.main.build_history", + return_value=([{"timestamp": 1, "price": 10.0}], "yahoo"), + ) as history_mock: + response = client.get( + "/markets/history", + params={"symbol": "AAPL", "period": "1mo", "interval": "1d"}, + ) + + assert response.status_code == 200 + assert response.json() == { + "symbol": "AAPL", + "period": "1mo", + "interval": "1d", + "source": "yahoo", + "points": [{"timestamp": 1, "price": 10.0}], + } + history_mock.assert_called_once_with("AAPL", "1mo", "1d") + + invalid_period = client.get( + "/markets/history", + params={"symbol": "AAPL", "period": "bad", "interval": "1d"}, + ) + assert invalid_period.status_code == 400 + assert invalid_period.json()["detail"] == "Unsupported period." + + invalid_interval = client.get( + "/markets/history", + params={"symbol": "AAPL", "period": "1mo", "interval": "bad"}, + ) + assert invalid_interval.status_code == 400 + assert invalid_interval.json()["detail"] == "Unsupported interval." + + +def test_history_route_uses_default_period_and_interval(): + with patch( + "src.main.build_history", + return_value=([{"timestamp": 1, "price": 10.0}], "yahoo"), + ) as history_mock: + response = client.get("/markets/history", params={"symbol": "AAPL"}) + + assert response.status_code == 200 + assert response.json()["period"] == "6mo" + assert response.json()["interval"] == "1d" + history_mock.assert_called_once_with("AAPL", "6mo", "1d") + + +def test_main_module_runs_uvicorn_when_executed_as_script(): + with patch.dict(os.environ, {"PORT": "9999"}), patch("uvicorn.run") as run_mock: + runpy.run_module("src.main", run_name="__main__") + + run_mock.assert_called_once() + assert run_mock.call_args.kwargs["port"] == 9999 diff --git a/app/server/services/python/market/test/test_service.py b/app/server/services/python/market/test/test_service.py new file mode 100644 index 0000000..f009734 --- /dev/null +++ b/app/server/services/python/market/test/test_service.py @@ -0,0 +1,447 @@ +from unittest.mock import patch + +from fastapi import HTTPException + +from src import service + + +def test_fetch_quote_snapshot_uses_market_price_and_previous_close(): + chart_result = { + "meta": { + "regularMarketPrice": 151.23, + "chartPreviousClose": 149.0, + } + } + points = [ + {"timestamp": 1700000000, "price": 149.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 151.23, + "change": 2.23, + "changePercent": 1.4966, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_handles_missing_meta_key(): + chart_result = {} + points = [ + {"timestamp": 1700000000, "price": 149.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 150.5, + "change": 1.5, + "changePercent": 1.0067, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_requests_the_expected_chart_defaults(): + chart_result = {"meta": {"regularMarketPrice": 151.23, "chartPreviousClose": 149.0}} + points = [ + {"timestamp": 1700000000, "price": 149.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result) as chart_mock, patch( + "src.service.extract_chart_points", return_value=points + ) as extract_mock: + service.fetch_quote_snapshot("AAPL") + + chart_mock.assert_called_once_with("AAPL", "1mo", "1d") + extract_mock.assert_called_once_with(chart_result) + + +def test_fetch_quote_snapshot_handles_missing_meta_key(): + chart_result = {} + points = [ + {"timestamp": 1700000000, "price": 149.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 150.5, + "change": 1.5, + "changePercent": 1.0067, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_falls_back_to_chart_points_when_meta_missing(): + chart_result = {"meta": {}} + points = [ + {"timestamp": 1700000000, "price": 149.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 150.5, + "change": 1.5, + "changePercent": 1.0067, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_falls_back_to_previous_close_meta(): + chart_result = {"meta": {"regularMarketPrice": 151.23, "previousClose": 150.0}} + points = [ + {"timestamp": 1700000000, "price": 149.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 151.23, + "change": 1.23, + "changePercent": 0.82, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_falls_back_to_previous_point_without_meta_closes(): + chart_result = {"meta": {}} + points = [ + {"timestamp": 1700000000, "price": 100.0}, + {"timestamp": 1700003600, "price": 110.0}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 110.0, + "change": 10.0, + "changePercent": 10.0, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_returns_none_change_with_single_point(): + chart_result = {"meta": {}} + points = [{"timestamp": 1700003600, "price": 150.5}] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 150.5, + "change": None, + "changePercent": None, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_prefers_chart_previous_close_over_previous_close(): + chart_result = { + "meta": { + "regularMarketPrice": 151.23, + "chartPreviousClose": 149.0, + "previousClose": 140.0, + } + } + points = [ + {"timestamp": 1700000000, "price": 140.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot["change"] == 2.23 + assert snapshot["changePercent"] == 1.4966 + + +def test_fetch_quote_snapshot_falls_back_when_meta_values_are_not_numeric(): + chart_result = { + "meta": { + "regularMarketPrice": "bad", + "chartPreviousClose": "bad", + "previousClose": "bad", + } + } + points = [ + {"timestamp": 1700000000, "price": 149.0}, + {"timestamp": 1700003600, "price": 150.5}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 150.5, + "change": 1.5, + "changePercent": 1.0067, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_preserves_zero_market_price_and_previous_close(): + chart_result = { + "meta": { + "regularMarketPrice": 0.0, + "chartPreviousClose": 0.0, + } + } + points = [ + {"timestamp": 1700000000, "price": 10.0}, + {"timestamp": 1700003600, "price": 12.0}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot == { + "symbol": "AAPL", + "price": 0.0, + "change": 0.0, + "changePercent": None, + "timestamp": 1700003600, + "source": "yahoo", + } + + +def test_fetch_quote_snapshot_uses_last_point_timestamp_and_price_fallback(): + chart_result = {"meta": {}} + points = [ + {"timestamp": 1700000000, "price": 100.0}, + {"timestamp": 1700003600, "price": 110.0}, + {"timestamp": 1700007200, "price": 120.0}, + ] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch( + "src.service.extract_chart_points", return_value=points + ): + snapshot = service.fetch_quote_snapshot("AAPL") + + assert snapshot["price"] == 120.0 + assert snapshot["timestamp"] == 1700007200 + assert snapshot["change"] == 10.0 + assert snapshot["changePercent"] == 9.0909 + + +def test_build_history_returns_points_and_source(): + chart_result = {"meta": {"symbol": "AAPL"}} + points = [{"timestamp": 1700000000, "price": 123.45}] + + with patch("src.service.fetch_yahoo_chart", return_value=chart_result) as chart_mock, patch( + "src.service.extract_chart_points", return_value=points + ) as extract_mock: + assert service.build_history("AAPL", "1mo", "1d") == (points, "yahoo") + chart_mock.assert_called_once_with("AAPL", "1mo", "1d") + extract_mock.assert_called_once_with(chart_result) + + +def test_enrich_search_results_skips_items_that_raise_http_exceptions(): + with patch( + "src.service.fetch_quote_snapshot", + side_effect=[ + { + "symbol": "AAPL", + "price": 100.0, + "change": 1.0, + "changePercent": 1.0, + "timestamp": 1700000000, + "source": "yahoo", + }, + HTTPException(status_code=404, detail="missing"), + ], + ): + enriched = service.enrich_search_results( + [ + {"symbol": "AAPL", "name": "Apple"}, + {"symbol": "MISS", "name": "Missing"}, + ] + ) + + assert enriched == [ + { + "symbol": "AAPL", + "name": "Apple", + "price": 100.0, + "change": 1.0, + "changePercent": 1.0, + "timestamp": 1700000000, + "source": "yahoo", + } + ] + + +def test_enrich_search_results_preserves_existing_fields(): + with patch( + "src.service.fetch_quote_snapshot", + return_value={ + "symbol": "AAPL", + "price": 100.0, + "change": 1.0, + "changePercent": 1.0, + "timestamp": 1700000000, + "source": "yahoo", + }, + ): + enriched = service.enrich_search_results( + [ + { + "symbol": "AAPL", + "name": "Apple", + "type": "stock", + "currency": "USD", + } + ] + ) + + assert enriched == [ + { + "symbol": "AAPL", + "name": "Apple", + "type": "stock", + "currency": "USD", + "price": 100.0, + "change": 1.0, + "changePercent": 1.0, + "timestamp": 1700000000, + "source": "yahoo", + } + ] + + +def test_enrich_search_results_returns_empty_for_empty_input(): + with patch("src.service.fetch_quote_snapshot") as snapshot_mock: + enriched = service.enrich_search_results([]) + + assert enriched == [] + snapshot_mock.assert_not_called() + + +def test_enrich_search_results_propagates_non_http_exceptions(): + with patch( + "src.service.fetch_quote_snapshot", + side_effect=RuntimeError("boom"), + ): + try: + service.enrich_search_results([{"symbol": "AAPL"}]) + except RuntimeError as exc: + assert str(exc) == "boom" + else: + raise AssertionError("Expected RuntimeError to propagate") + + +def test_enrich_search_results_calls_snapshot_for_each_symbol_in_order(): + with patch( + "src.service.fetch_quote_snapshot", + side_effect=[ + { + "symbol": "AAPL", + "price": 1.0, + "change": 0.1, + "changePercent": 10.0, + "timestamp": 1, + "source": "yahoo", + }, + { + "symbol": "MSFT", + "price": 2.0, + "change": 0.2, + "changePercent": 11.0, + "timestamp": 2, + "source": "yahoo", + }, + ], + ) as snapshot_mock: + enriched = service.enrich_search_results( + [ + {"symbol": "AAPL", "name": "Apple"}, + {"symbol": "MSFT", "name": "Microsoft"}, + ] + ) + + assert [item["symbol"] for item in enriched] == ["AAPL", "MSFT"] + assert [call.args[0] for call in snapshot_mock.call_args_list] == ["AAPL", "MSFT"] + + +def test_enrich_search_results_skips_failed_item_and_continues_afterward(): + with patch( + "src.service.fetch_quote_snapshot", + side_effect=[ + HTTPException(status_code=404, detail="missing"), + { + "symbol": "MSFT", + "price": 2.0, + "change": 0.2, + "changePercent": 11.0, + "timestamp": 2, + "source": "yahoo", + }, + ], + ): + enriched = service.enrich_search_results( + [ + {"symbol": "MISS", "name": "Missing"}, + {"symbol": "MSFT", "name": "Microsoft"}, + ] + ) + + assert enriched == [ + { + "symbol": "MSFT", + "name": "Microsoft", + "price": 2.0, + "change": 0.2, + "changePercent": 11.0, + "timestamp": 2, + "source": "yahoo", + } + ] diff --git a/app/server/services/python/market/test/test_utils.py b/app/server/services/python/market/test/test_utils.py new file mode 100644 index 0000000..31b4928 --- /dev/null +++ b/app/server/services/python/market/test/test_utils.py @@ -0,0 +1,117 @@ +import os +import time +from datetime import datetime, timezone + +from src import utils + + +class FakeTimestamp: + def __init__(self, value: datetime): + self.value = value + + def to_pydatetime(self) -> datetime: + return self.value + + +class TrackingTimestamp: + def __init__(self, value: datetime): + self.value = value + self.calls = 0 + + def to_pydatetime(self) -> datetime: + self.calls += 1 + return self.value + + +class BadTimestamp: + def __init__(self, value): + self.value = value + + def to_pydatetime(self): + return self.value + + +def test_is_number_and_to_float_helpers(): + assert utils.is_number(3.14) is True + assert utils.is_number("3.14") is False + assert utils.is_number(float("nan")) is False + assert utils.to_float(12.3456789) == 12.345679 + assert utils.to_float("12") is None + + +def test_to_timestamp_handles_datetime_like_objects(): + timestamp = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc) + naive_timestamp = datetime(2024, 1, 1, 12, 0) + assert utils.to_timestamp(timestamp) == int(timestamp.timestamp()) + assert utils.to_timestamp(naive_timestamp) == int( + naive_timestamp.replace(tzinfo=timezone.utc).timestamp() + ) + assert utils.to_timestamp(FakeTimestamp(timestamp)) == int(timestamp.timestamp()) + assert utils.to_timestamp(None) is None + assert utils.to_timestamp("bad-value") is None + + +def test_to_timestamp_calls_to_pydatetime_once_and_normalizes_naive_values(): + naive_timestamp = datetime(2024, 1, 1, 12, 0) + wrapped = TrackingTimestamp(naive_timestamp) + + assert utils.to_timestamp(wrapped) == int( + naive_timestamp.replace(tzinfo=timezone.utc).timestamp() + ) + assert wrapped.calls == 1 + + +def test_to_timestamp_returns_none_when_to_pydatetime_is_not_a_datetime(): + assert utils.to_timestamp(BadTimestamp("not-a-datetime")) is None + + +def test_to_timestamp_preserves_timezone_aware_offsets(): + offset_timestamp = datetime.fromisoformat("2024-01-01T12:00:00+02:00") + + assert utils.to_timestamp(offset_timestamp) == int(offset_timestamp.timestamp()) + + +def test_to_timestamp_treats_naive_datetimes_as_utc_even_when_local_tz_differs(): + original_tz = os.environ.get("TZ") + try: + os.environ["TZ"] = "America/Winnipeg" + time.tzset() + naive_timestamp = datetime(2024, 1, 1, 12, 0) + + assert utils.to_timestamp(naive_timestamp) == int( + naive_timestamp.replace(tzinfo=timezone.utc).timestamp() + ) + finally: + if original_tz is None: + os.environ.pop("TZ", None) + else: + os.environ["TZ"] = original_tz + time.tzset() + + +def test_format_change_and_normalize_query(): + assert utils.format_change(11.0, 10.0) == (1.0, 10.0) + assert utils.format_change(11.0, 0.0) == (11.0, None) + assert utils.format_change(None, 10.0) == (None, None) + assert utils.normalize_query("EUR/USD Inc.") == "eurusdinc" + + +def test_is_number_rejects_infinity_values(): + assert utils.is_number(float("inf")) is False + assert utils.is_number(float("-inf")) is False + + +def test_format_change_returns_none_when_previous_missing(): + assert utils.format_change(11.0, None) == (None, None) + + +def test_format_change_handles_negative_moves(): + assert utils.format_change(9.0, 10.0) == (-1.0, -10.0) + + +def test_normalize_query_preserves_digits_while_removing_symbols(): + assert utils.normalize_query("BTC-USD 2024!") == "btcusd2024" + + +def test_format_change_applies_rounding_precision(): + assert utils.format_change(10.1234567, 9.7654321) == (0.358025, 3.6662) diff --git a/app/server/services/python/market/test/test_yahoo_client.py b/app/server/services/python/market/test/test_yahoo_client.py new file mode 100644 index 0000000..ed00cf0 --- /dev/null +++ b/app/server/services/python/market/test/test_yahoo_client.py @@ -0,0 +1,783 @@ +from unittest.mock import MagicMock, patch + +import pytest +from fastapi import HTTPException + +from src import yahoo_client + + +def build_response(payload): + response = MagicMock() + response.raise_for_status.return_value = None + response.json.return_value = payload + return response + + +def test_fetch_yahoo_chart_returns_first_result(): + payload = {"chart": {"result": [{"meta": {"symbol": "AAPL"}}], "error": None}} + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get: + result = yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert result == {"meta": {"symbol": "AAPL"}} + mock_get.assert_called_once_with( + yahoo_client.YAHOO_CHART_URL.format(symbol="AAPL"), + params={ + "range": "1mo", + "interval": "1d", + "includePrePost": "false", + "events": "div,splits", + }, + headers=yahoo_client.REQUEST_HEADERS, + timeout=8, + ) + + +def test_fetch_yahoo_chart_handles_request_and_json_errors(): + with patch("src.yahoo_client.requests.get", side_effect=ValueError("bad json")): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 502 + assert exc_info.value.detail == "Unable to reach Yahoo market data." + + with patch( + "src.yahoo_client.requests.get", + side_effect=yahoo_client.requests.RequestException("network"), + ): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 502 + assert exc_info.value.detail == "Unable to reach Yahoo market data." + + +def test_fetch_yahoo_chart_handles_raise_for_status_errors(): + response = MagicMock() + response.raise_for_status.side_effect = yahoo_client.requests.RequestException("bad status") + + with patch("src.yahoo_client.requests.get", return_value=response): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 502 + assert exc_info.value.detail == "Unable to reach Yahoo market data." + + +def test_fetch_yahoo_chart_prefers_first_result_when_multiple_exist(): + payload = { + "chart": { + "result": [{"meta": {"symbol": "FIRST"}}, {"meta": {"symbol": "SECOND"}}], + "error": None, + } + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + result = yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert result == {"meta": {"symbol": "FIRST"}} + + +def test_fetch_yahoo_chart_quotes_symbols_with_reserved_characters(): + payload = {"chart": {"result": [{"meta": {"symbol": "EUR/USD"}}], "error": None}} + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get: + yahoo_client.fetch_yahoo_chart("EUR/USD", "1mo", "1d") + + assert mock_get.call_args.args[0].endswith("/EUR%2FUSD") + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get: + yahoo_client.fetch_yahoo_chart("EURUSD=X", "1mo", "1d") + + assert mock_get.call_args.args[0].endswith("/EURUSD=X") + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get: + yahoo_client.fetch_yahoo_chart("^GSPC.-", "1mo", "1d") + + assert mock_get.call_args.args[0].endswith("/^GSPC.-") + + +def test_fetch_yahoo_chart_handles_yahoo_error_payload(): + payload = {"chart": {"result": None, "error": {"description": "missing"}}} + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "missing" + + +def test_fetch_yahoo_chart_uses_default_message_when_description_missing(): + payload = {"chart": {"result": None, "error": {"description": None}}} + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Yahoo market data not found for symbol." + + +def test_fetch_yahoo_chart_handles_missing_results(): + payload = {"chart": {"result": [], "error": None}} + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Yahoo market data not found for symbol." + + +def test_fetch_yahoo_chart_handles_missing_chart_payload(): + with patch("src.yahoo_client.requests.get", return_value=build_response({})): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Yahoo market data not found for symbol." + + +def test_fetch_yahoo_chart_handles_chart_none_payload(): + with patch("src.yahoo_client.requests.get", return_value=build_response({"chart": None})): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d") + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Yahoo market data not found for symbol." + + +def test_extract_chart_points_success_and_failures(): + result = yahoo_client.extract_chart_points( + { + "timestamp": [1700000000, 1700003600], + "indicators": {"quote": [{"close": [123.45, 124.0]}]}, + } + ) + assert result == [ + {"timestamp": 1700000000, "price": 123.45}, + {"timestamp": 1700003600, "price": 124.0}, + ] + + with pytest.raises(HTTPException): + yahoo_client.extract_chart_points({"indicators": {}}) + + with pytest.raises(HTTPException): + yahoo_client.extract_chart_points( + {"timestamp": [1], "indicators": {"quote": [{"close": [None]}]}} + ) + + with pytest.raises(HTTPException): + yahoo_client.extract_chart_points( + {"timestamp": [1], "indicators": {"quote": [{"open": [10.0]}]}} + ) + + +def test_extract_chart_points_uses_only_the_first_quote_entry(): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.extract_chart_points( + { + "timestamp": [1], + "indicators": { + "quote": [ + {"close": [None]}, + {"close": [123.45]}, + ] + }, + } + ) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Historical data not found for symbol." + + +def test_extract_chart_points_handles_first_quote_none(): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.extract_chart_points( + { + "timestamp": [1], + "indicators": {"quote": [None]}, + } + ) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Historical data not found for symbol." + + +def test_extract_chart_points_preserves_zero_price_points(): + result = yahoo_client.extract_chart_points( + { + "timestamp": [1, 2], + "indicators": {"quote": [{"close": [0.0, 1.5]}]}, + } + ) + + assert result == [ + {"timestamp": 1, "price": 0.0}, + {"timestamp": 2, "price": 1.5}, + ] + + +def test_extract_chart_points_skips_non_numeric_values(): + result = yahoo_client.extract_chart_points( + { + "timestamp": [1, 2, 3], + "indicators": {"quote": [{"close": [10.0, "bad", 12.0]}]}, + } + ) + + assert result == [ + {"timestamp": 1, "price": 10.0}, + {"timestamp": 3, "price": 12.0}, + ] + + +def test_extract_chart_points_skips_invalid_prices_but_keeps_valid_points(): + result = yahoo_client.extract_chart_points( + { + "timestamp": [1700000000, 1700003600, 1700007200], + "indicators": {"quote": [{"close": [123.45, None, 124.0]}]}, + } + ) + + assert result == [ + {"timestamp": 1700000000, "price": 123.45}, + {"timestamp": 1700007200, "price": 124.0}, + ] + + +def test_extract_chart_points_casts_timestamps_to_ints(): + result = yahoo_client.extract_chart_points( + { + "timestamp": ["1700000000"], + "indicators": {"quote": [{"close": [123.45]}]}, + } + ) + + assert result == [{"timestamp": 1700000000, "price": 123.45}] + + +def test_extract_chart_points_handles_missing_timestamps(): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.extract_chart_points( + {"indicators": {"quote": [{"close": [123.45]}]}} + ) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Historical data not found for symbol." + + +def test_extract_chart_points_handles_missing_indicators(): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.extract_chart_points({"timestamp": [1]}) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Historical data not found for symbol." + + +def test_extract_chart_points_truncates_to_shortest_series(): + result = yahoo_client.extract_chart_points( + { + "timestamp": [1, 2, 3], + "indicators": {"quote": [{"close": [10.0, 11.0]}]}, + } + ) + + assert result == [ + {"timestamp": 1, "price": 10.0}, + {"timestamp": 2, "price": 11.0}, + ] + + +def test_extract_chart_points_raises_when_quote_list_is_empty(): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.extract_chart_points( + {"timestamp": [1], "indicators": {"quote": []}} + ) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Historical data not found for symbol." + + +def test_extract_chart_points_raises_when_all_numeric_points_are_filtered_out(): + with pytest.raises(HTTPException) as exc_info: + yahoo_client.extract_chart_points( + { + "timestamp": [1, 2], + "indicators": {"quote": [{"close": ["bad", None]}]}, + } + ) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Historical data not found for symbol." + + +def test_search_catalog_helpers_cover_empty_and_filtered_paths(): + assert len(yahoo_client.search_forex_catalog("", 2)) == 2 + assert yahoo_client.search_forex_catalog("euro", 5)[0]["symbol"] == "EURUSD=X" + assert len(yahoo_client.search_stock_catalog("", 2)) == 2 + assert yahoo_client.search_stock_catalog("tesla", 5)[0]["symbol"] == "TSLA" + + +def test_search_catalog_helpers_honor_limit_for_matching_results(): + assert yahoo_client.search_forex_catalog("usd", 1) == [yahoo_client.FOREX_CATALOG[0]] + assert yahoo_client.search_stock_catalog("inc", 2) == [ + yahoo_client.FEATURED_STOCKS[0], + yahoo_client.FEATURED_STOCKS[3], + ] + + +def test_search_catalog_helpers_return_empty_for_unknown_queries(): + assert yahoo_client.search_forex_catalog("zzzz", 5) == [] + assert yahoo_client.search_stock_catalog("zzzz", 5) == [] + + +def test_yahoo_search_filters_deduplicates_and_falls_back_on_error(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Inc.", + "currency": "USD", + "exchange": "NMS", + }, + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Duplicate", + "currency": "USD", + "exchange": "NMS", + }, + { + "quoteType": "CURRENCY", + "symbol": "EURUSD=X", + "shortname": "Euro FX", + "currency": "USD", + "exchange": "CCY", + }, + { + "quoteType": "MUTUALFUND", + "symbol": "SKIP", + }, + { + "quoteType": "EQUITY", + "symbol": None, + }, + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("apple", 10) + + assert results == [ + { + "symbol": "AAPL", + "displaySymbol": "AAPL", + "name": "Apple Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "EURUSD=X", + "displaySymbol": "EURUSD", + "name": "Euro FX", + "type": "forex", + "currency": "USD", + "exchange": "CCY", + }, + ] + + with patch("src.yahoo_client.requests.get", side_effect=RuntimeError("boom")): + with pytest.raises(RuntimeError): + yahoo_client.yahoo_search("apple", 10) + + with patch( + "src.yahoo_client.requests.get", + side_effect=yahoo_client.requests.RequestException("network"), + ): + assert yahoo_client.yahoo_search("apple", 10) == [] + + with patch("src.yahoo_client.requests.get", side_effect=ValueError("bad json")): + assert yahoo_client.yahoo_search("apple", 10) == [] + + +def test_yahoo_search_skips_quotes_without_symbols_or_supported_types(): + payload = { + "quotes": [ + {"quoteType": "EQUITY", "symbol": None}, + {"quoteType": "MUTUALFUND", "symbol": "SKIP"}, + {"quoteType": None, "symbol": "ALSO_SKIP"}, + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("apple", 10) + + assert results == [] + + +def test_yahoo_search_continues_after_missing_symbol_and_unsupported_type(): + payload = { + "quotes": [ + {"quoteType": "EQUITY", "symbol": None}, + {"quoteType": "MUTUALFUND", "symbol": "SKIP"}, + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Inc.", + "currency": "USD", + "exchange": "NMS", + }, + { + "quoteType": "CURRENCY", + "symbol": "EURUSD=X", + "shortname": "Euro FX", + "currency": "USD", + "exchange": "CCY", + }, + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("mixed", 10) + + assert results == [ + { + "symbol": "AAPL", + "displaySymbol": "AAPL", + "name": "Apple Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "EURUSD=X", + "displaySymbol": "EURUSD", + "name": "Euro FX", + "type": "forex", + "currency": "USD", + "exchange": "CCY", + }, + ] + + +def test_yahoo_search_returns_empty_when_quotes_key_is_missing(): + with patch("src.yahoo_client.requests.get", return_value=build_response({})): + results = yahoo_client.yahoo_search("apple", 10) + + assert results == [] + + +def test_yahoo_search_returns_empty_when_quotes_is_none(): + with patch( + "src.yahoo_client.requests.get", + return_value=build_response({"quotes": None}), + ): + results = yahoo_client.yahoo_search("apple", 10) + + assert results == [] + + +def test_yahoo_search_uses_expected_request_shape(): + payload = {"quotes": []} + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get: + results = yahoo_client.yahoo_search("apple", 3) + + assert results == [] + mock_get.assert_called_once_with( + yahoo_client.YAHOO_SEARCH_URL, + params={"q": "apple", "quotesCount": 6, "newsCount": 0}, + headers=yahoo_client.REQUEST_HEADERS, + timeout=6, + ) + + +def test_yahoo_search_uses_shortname_when_longname_missing(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "shortname": "Apple Short", + "currency": "USD", + "exchange": "NMS", + } + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("apple", 10) + + assert results == [ + { + "symbol": "AAPL", + "displaySymbol": "AAPL", + "name": "Apple Short", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + } + ] + + +def test_yahoo_search_skips_empty_string_symbol(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "", + "longname": "Empty", + "currency": "USD", + "exchange": "NMS", + } + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("empty", 10) + + assert results == [] + + +def test_yahoo_search_uses_symbol_when_names_are_missing_for_forex(): + payload = { + "quotes": [ + { + "quoteType": "CURRENCY", + "symbol": "USDJPY=X", + "currency": "JPY", + "exchange": "CCY", + } + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("yen", 10) + + assert results == [ + { + "symbol": "USDJPY=X", + "displaySymbol": "USDJPY", + "name": "USDJPY=X", + "type": "forex", + "currency": "JPY", + "exchange": "CCY", + } + ] + + +def test_yahoo_search_formats_forex_display_symbol_and_preserves_exchange_none(): + payload = { + "quotes": [ + { + "quoteType": "CURRENCY", + "symbol": "EURUSD=X", + "shortname": "Euro FX", + "currency": "", + "exchange": None, + } + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("eur", 10) + + assert results == [ + { + "symbol": "EURUSD=X", + "displaySymbol": "EURUSD", + "name": "Euro FX", + "type": "forex", + "currency": "USD", + "exchange": None, + } + ] + + +def test_yahoo_search_prefers_longname_over_shortname(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Long", + "shortname": "Apple Short", + "currency": "USD", + "exchange": "NMS", + } + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("apple", 10) + + assert results[0]["name"] == "Apple Long" + + +def test_yahoo_search_keeps_equity_display_symbol_even_when_symbol_ends_with_x(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "ABC=X", + "longname": "Equity With X", + "currency": "USD", + "exchange": "NMS", + } + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("abc", 10) + + assert results == [ + { + "symbol": "ABC=X", + "displaySymbol": "ABC=X", + "name": "Equity With X", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + } + ] + + +def test_yahoo_search_deduplicates_before_applying_limit(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Inc.", + "currency": "USD", + "exchange": "NMS", + }, + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Duplicate", + "currency": "USD", + "exchange": "NMS", + }, + { + "quoteType": "EQUITY", + "symbol": "MSFT", + "longname": "Microsoft Corporation", + "currency": "USD", + "exchange": "NMS", + }, + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("tech", 2) + + assert results == [ + { + "symbol": "AAPL", + "displaySymbol": "AAPL", + "name": "Apple Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + { + "symbol": "MSFT", + "displaySymbol": "MSFT", + "name": "Microsoft Corporation", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + }, + ] + + +def test_yahoo_search_preserves_result_order_across_supported_types(): + payload = { + "quotes": [ + { + "quoteType": "CURRENCY", + "symbol": "EURUSD=X", + "shortname": "Euro FX", + "currency": "USD", + "exchange": "CCY", + }, + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Inc.", + "currency": "USD", + "exchange": "NMS", + }, + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("mixed", 10) + + assert [item["symbol"] for item in results] == ["EURUSD=X", "AAPL"] + + +def test_yahoo_search_stops_when_limit_is_reached(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "AAPL", + "longname": "Apple Inc.", + "currency": "USD", + "exchange": "NMS", + }, + { + "quoteType": "CURRENCY", + "symbol": "EURUSD=X", + "shortname": "Euro FX", + "currency": "USD", + "exchange": "CCY", + }, + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("a", 1) + + assert results == [ + { + "symbol": "AAPL", + "displaySymbol": "AAPL", + "name": "Apple Inc.", + "type": "stock", + "currency": "USD", + "exchange": "NMS", + } + ] + + +def test_yahoo_search_falls_back_to_symbol_name_and_usd_currency(): + payload = { + "quotes": [ + { + "quoteType": "EQUITY", + "symbol": "BRK-B", + "exchange": "NYQ", + } + ] + } + + with patch("src.yahoo_client.requests.get", return_value=build_response(payload)): + results = yahoo_client.yahoo_search("berkshire", 10) + + assert results == [ + { + "symbol": "BRK-B", + "displaySymbol": "BRK-B", + "name": "BRK-B", + "type": "stock", + "currency": "USD", + "exchange": "NYQ", + } + ] diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 072a253..65cf47e 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -3,10 +3,40 @@ import { onExit } from "@/hooks"; import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; import { buildCorsConfig } from "@/expressUtils"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { randomUUID } from "node:crypto"; const app = express(); app.use(buildCorsConfig()); +const CLIENT_STATE_DIR = + process.env.CLIENT_STATE_DIR ?? "/var/lib/finus-client-state"; +const CLIENT_STATE_RESET_KEY_PATH = path.join( + CLIENT_STATE_DIR, + "reset-key.txt", +); + +function resolveClientStateResetKey() { + try { + const existingKey = fs + .readFileSync(CLIENT_STATE_RESET_KEY_PATH, "utf8") + .trim(); + if (existingKey.length > 0) { + return existingKey; + } + } catch { + // Fall through and create a new reset key. + } + + const nextKey = randomUUID(); + fs.mkdirSync(CLIENT_STATE_DIR, { recursive: true }); + fs.writeFileSync(CLIENT_STATE_RESET_KEY_PATH, nextKey, "utf8"); + return nextKey; +} + +const clientStateResetKey = resolveClientStateResetKey(); + const pathMatches = (path, valid) => { path = path.replace("/api/", ""); return valid.includes(path); @@ -32,6 +62,16 @@ app.use( }), ); +const MARKET_PATHS = ["markets/search", "markets/quote", "markets/history"]; +app.use( + createProxyMiddleware({ + pathFilter: (path) => pathMatches(path, MARKET_PATHS), + target: process.env.MARKET_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { "^/api": "" }, + }), +); + app.use( createProxyMiddleware({ pathFilter: "/api/transactions", @@ -68,6 +108,13 @@ app.get("/health", async (req: express.Request, res: express.Response) => { res.json(result); }); +app.get( + "/client-state/reset-key", + (_req: express.Request, res: express.Response) => { + res.json({ resetKey: clientStateResetKey }); + }, +); + const server = app.listen(PORT, () => { console.log(`API Gateway running on port ${PORT}`); }); @@ -143,3 +190,12 @@ app.use( changeOrigin: true, }), ); + +// market search, quote, and history endpoints from market service +app.use( + createProxyMiddleware({ + pathFilter: ["/markets/search", "/markets/quote", "/markets/history"], + target: process.env.MARKET_SERVICE_ADDR, + changeOrigin: true, + }), +); diff --git a/app/server/services/ts/market/.gitignore b/app/server/services/ts/market/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/app/server/services/ts/market/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/app/server/services/ts/market/Dockerfile b/app/server/services/ts/market/Dockerfile new file mode 100644 index 0000000..600d6cb --- /dev/null +++ b/app/server/services/ts/market/Dockerfile @@ -0,0 +1,21 @@ +FROM oven/bun:1.3.10 + +WORKDIR /project +COPY package.json bun.lockb . +RUN --mount=type=cache,target=/root/.bun/install/cache \ +bun install + +WORKDIR /project/app +COPY market/package.json market/bun.lockb . +RUN --mount=type=cache,target=/root/.bun/install/cache \ +bun install + +WORKDIR /project +COPY tsconfig.json . +COPY common ./common + +WORKDIR /project/app +COPY market/src ./src + +CMD ["bun", "--no-env-file", "src/index.ts"] + diff --git a/app/server/services/ts/market/Dockerfile.test b/app/server/services/ts/market/Dockerfile.test new file mode 100644 index 0000000..a8718cd --- /dev/null +++ b/app/server/services/ts/market/Dockerfile.test @@ -0,0 +1,11 @@ +FROM measureonecodetwice/finus-user:local + +COPY market /project/market + +COPY vitest.config.ts /project/vitest.config.ts + +WORKDIR /project + +ENV CI=true + +CMD ["bun", "x", "vitest", "--run"] diff --git a/app/server/services/ts/market/README.md b/app/server/services/ts/market/README.md new file mode 100644 index 0000000..6431ed1 --- /dev/null +++ b/app/server/services/ts/market/README.md @@ -0,0 +1,15 @@ +# market + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/app/server/services/ts/market/bun.lock b/app/server/services/ts/market/bun.lock new file mode 100644 index 0000000..c8b4458 --- /dev/null +++ b/app/server/services/ts/market/bun.lock @@ -0,0 +1,179 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "market", + "dependencies": { + "vitest": "^4.1.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.11", "", { "os": "android", "cpu": "arm64" }, "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm" }, "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.11", "", { "os": "none", "cpu": "arm64" }, "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "x64" }, "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.11", "", {}, "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="], + + "@vitest/runner": ["@vitest/runner@4.1.1", "", { "dependencies": { "@vitest/utils": "4.1.1", "pathe": "^2.0.3" } }, "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg=="], + + "@vitest/spy": ["@vitest/spy@4.1.1", "", {}, "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA=="], + + "@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "rolldown": ["rolldown@1.0.0-rc.11", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.11" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-x64": "1.0.0-rc.11", "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "vite": ["vite@8.0.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA=="], + + "vitest": ["vitest@4.1.1", "", { "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", "@vitest/pretty-format": "4.1.1", "@vitest/runner": "4.1.1", "@vitest/snapshot": "4.1.1", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.1", "@vitest/browser-preview": "4.1.1", "@vitest/browser-webdriverio": "4.1.1", "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + } +} diff --git a/app/server/services/ts/market/index.ts b/app/server/services/ts/market/index.ts new file mode 100644 index 0000000..2a5e4b8 --- /dev/null +++ b/app/server/services/ts/market/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); diff --git a/app/server/services/ts/market/package.json b/app/server/services/ts/market/package.json new file mode 100644 index 0000000..1ebfd03 --- /dev/null +++ b/app/server/services/ts/market/package.json @@ -0,0 +1,15 @@ +{ + "name": "market", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "vitest": "^4.1.1" + } +} diff --git a/app/server/services/ts/market/tests/market.test.ts b/app/server/services/ts/market/tests/market.test.ts new file mode 100644 index 0000000..6e3667b --- /dev/null +++ b/app/server/services/ts/market/tests/market.test.ts @@ -0,0 +1,141 @@ +import { + getIntervalForPeriod, + formatMarketPrice, + formatSignedValue, + formatSignedPercent, + formatMarketTimestamp, + isPositiveChange, + toChartSeries, + mergeInstrumentQuote, +} from "../../../../../client/src/utils/market.ts"; + +import { describe, it, expect } from "vitest"; + +describe("getIntervalForPeriod", () => { + it("returns correct intervals", () => { + expect(getIntervalForPeriod("1d")).toBe("5m"); + expect(getIntervalForPeriod("5d")).toBe("15m"); + expect(getIntervalForPeriod("1y")).toBe("1wk"); + expect(getIntervalForPeriod("5y")).toBe("1mo"); + expect(getIntervalForPeriod("1mo")).toBe("1d"); + }); +}); + +describe("formatMarketPrice", () => { + it("returns N/A for null", () => { + expect(formatMarketPrice(null)).toBe("N/A"); + }); + + it("formats USD with correct precision", () => { + expect(formatMarketPrice(123.45, "USD")).toBe("$123.45"); + expect(formatMarketPrice(0.1234, "USD")).toBe("$0.1234"); + }); + + it("formats JPY correctly", () => { + expect(formatMarketPrice(500, "JPY")).toBe("¥500"); + }); + + it("treats any 3‑letter code as a currency (Intl formatting)", () => { + const result = formatMarketPrice(1.234567, "XYZ"); + expect(result.endsWith("1.2346")).toBe(true); + expect(result.startsWith("XYZ")).toBe(true); + }); +}); + +describe("formatSignedValue", () => { + it("handles null", () => { + expect(formatSignedValue(null)).toBe("N/A"); + }); + + it("adds + for positive values", () => { + expect(formatSignedValue(1.23)).toBe("+1.23"); + }); + + it("keeps negative sign", () => { + expect(formatSignedValue(-2.5)).toBe("-2.50"); + }); +}); + +describe("formatSignedPercent", () => { + it("handles null", () => { + expect(formatSignedPercent(null)).toBe("N/A"); + }); + + it("formats positive percent", () => { + expect(formatSignedPercent(1.234)).toBe("+1.23%"); + }); + + it("formats negative percent", () => { + expect(formatSignedPercent(-0.5)).toBe("-0.50%"); + }); +}); + +describe("formatMarketTimestamp", () => { + it("returns Unavailable for null", () => { + expect(formatMarketTimestamp(null)).toBe("Unavailable"); + }); + + it("formats a valid timestamp", () => { + const ts = 1700000000; + const result = formatMarketTimestamp(ts); + expect(result).toBeTypeOf("string"); + expect(result.length).toBeGreaterThan(5); + }); +}); + +describe("isPositiveChange", () => { + it("treats null as zero", () => { + expect(isPositiveChange(null)).toBe(true); + }); + + it("detects positive", () => { + expect(isPositiveChange(5)).toBe(true); + }); + + it("detects negative", () => { + expect(isPositiveChange(-1)).toBe(false); + }); +}); + +describe("toChartSeries", () => { + it("converts points to chart series", () => { + const points = [ + { timestamp: 1700000000, price: 100 }, + { timestamp: 1700003600, price: 105 }, + ]; + + const series = toChartSeries(points); + + expect(series.length).toBe(2); + expect(series[0]).toHaveProperty("date"); + expect(series[0]).toHaveProperty("label"); + expect(series[0]).toHaveProperty("price", 100); + }); +}); + +describe("mergeInstrumentQuote", () => { + it("merges quote fields into instrument", () => { + const instrument = { + symbol: "AAPL", + name: "Apple", + price: 0, + change: 0, + changePercent: 0, + timestamp: 0, + }; + + const quote = { + price: 150, + change: 2, + changePercent: 1.5, + timestamp: 1700000000, + }; + + const merged = mergeInstrumentQuote(instrument, quote); + + expect(merged.price).toBe(150); + expect(merged.change).toBe(2); + expect(merged.changePercent).toBe(1.5); + expect(merged.timestamp).toBe(1700000000); + }); +}); diff --git a/app/server/services/ts/market/tsconfig.json b/app/server/services/ts/market/tsconfig.json new file mode 100644 index 0000000..72a9658 --- /dev/null +++ b/app/server/services/ts/market/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/app/server/services/ts/market/types/HistoricalPoint.ts b/app/server/services/ts/market/types/HistoricalPoint.ts new file mode 100644 index 0000000..b714b8f --- /dev/null +++ b/app/server/services/ts/market/types/HistoricalPoint.ts @@ -0,0 +1,5 @@ +// timestamp, price model for charts +export interface HistoricalPoint { + timestamp: number; + price: number; +} diff --git a/app/server/services/ts/market/types/MarketModel.ts b/app/server/services/ts/market/types/MarketModel.ts new file mode 100644 index 0000000..ad2b881 --- /dev/null +++ b/app/server/services/ts/market/types/MarketModel.ts @@ -0,0 +1,9 @@ +// model for current market data +export interface MarketModel { + pair: string; + name: string; + type: "stock" | "forex"; + price: number; + shift: number; + timestamp: number; +} diff --git a/app/server/services/ts/user/Dockerfile.integration.test b/app/server/services/ts/user/Dockerfile.integration.test new file mode 100644 index 0000000..72a01b0 --- /dev/null +++ b/app/server/services/ts/user/Dockerfile.integration.test @@ -0,0 +1,9 @@ +FROM measureonecodetwice/finus-user:local + +COPY user/integration ./integration + +WORKDIR /project +COPY vitest.config.ts . + +ENV CI=true +CMD ["bun", "run", "vitest", "--run", "integration"] diff --git a/app/server/services/ts/user/bun.lock b/app/server/services/ts/user/bun.lock index d88bf07..ef41a73 100644 --- a/app/server/services/ts/user/bun.lock +++ b/app/server/services/ts/user/bun.lock @@ -10,7 +10,7 @@ "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mysql2": "^3.15.3", - "vitest": "^4.0.18", + "vitest": "^4.1.1", }, "devDependencies": { "@eslint/js": "^9.39.2", @@ -23,6 +23,7 @@ "eslint-plugin-react": "^7.37.5", "globals": "^17.3.0", "jiti": "^2.6.1", + "supertest": "^7.2.2", "typescript-eslint": "^8.54.0", }, "peerDependencies": { @@ -113,6 +114,10 @@ "@mysql/xdevapi": ["@mysql/xdevapi@8.0.35", "", { "dependencies": { "google-protobuf": "3.19.4", "lossless-json": "2.0.1", "parsimmon": "1.18.1" } }, "sha512-l7HoBt1l0GwdCBXBrqri4kPBbJrqsyvaa4eqXvq50aWWArOTc5XGV06FWF1epxT+RORBy7scZTWFANxpr4Sfuw=="], + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], @@ -215,19 +220,19 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], - "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + "@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="], - "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + "@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="], - "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + "@vitest/runner": ["@vitest/runner@4.1.1", "", { "dependencies": { "@vitest/utils": "4.1.1", "pathe": "^2.0.3" } }, "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A=="], - "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg=="], - "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + "@vitest/spy": ["@vitest/spy@4.1.1", "", {}, "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA=="], - "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -255,6 +260,8 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], @@ -297,16 +304,22 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -329,6 +342,8 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -347,7 +362,7 @@ "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], @@ -395,6 +410,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -413,6 +430,8 @@ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -591,6 +610,10 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -739,7 +762,7 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -755,6 +778,10 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], + + "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -799,7 +826,7 @@ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "vitest": ["vitest@4.1.1", "", { "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", "@vitest/pretty-format": "4.1.1", "@vitest/runner": "4.1.1", "@vitest/snapshot": "4.1.1", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.1", "@vitest/browser-preview": "4.1.1", "@vitest/browser-webdriverio": "4.1.1", "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], diff --git a/app/server/services/ts/user/integration/accounts.test.ts b/app/server/services/ts/user/integration/accounts.test.ts new file mode 100644 index 0000000..00743ea --- /dev/null +++ b/app/server/services/ts/user/integration/accounts.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import request from "supertest"; +import { createAuthenticatedAccount, createSignupBody } from "./setup"; + +const BASE_URL = process.env.API_GATEWAY_ADDR; + +let token: string; +let accountId: number; + +beforeAll(async () => { + const setup = await createAuthenticatedAccount(BASE_URL, "accounts", { + name: "Chequing", + type: "chequing", + balance: 1000, + value: 1000, + subtype: "na", + }); + + token = setup.token; + accountId = setup.accountId; +}, 30000); + +describe("Accounts Integration (Docker)", () => { + it("lists accounts for the authenticated user", async () => { + const res = await request(BASE_URL) + .get("/api/accounts") + .set("Authorization", `Bearer ${token}`); + + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + }); + + it("updates an account", async () => { + const res = await request(BASE_URL) + .put("/api/accounts") + .set("Authorization", `Bearer ${token}`) + .send({ + id: accountId, + name: "Updated Chequing", + type: "chequing", + balance: 1500, + value: 1500, + subtype: "na", + }); + + expect(res.status).toBe(200); + expect(res.body.message).toBe("Account successfully updated"); + }); + + it("prevents unauthorized account updates", async () => { + const otherUser = createSignupBody("accounts-other"); + otherUser.age = 22; + otherUser.password = "password123"; + + const signup = await request(BASE_URL).post("/api/signup").send(otherUser); + expect(signup.status).toBe(201); + + const login2 = await request(BASE_URL) + .post("/api/login") + .send({ email: otherUser.email, password: otherUser.password }); + + expect(login2.status).toBe(200); + + const token2 = login2.body.token; + + const res = await request(BASE_URL) + .put("/api/accounts") + .set("Authorization", `Bearer ${token2}`) + .send({ + id: accountId, + name: "Hacked Account", + type: "chequing", + balance: 9999, + value: 9999, + subtype: "na", + }); + + expect(res.status).toBe(401); + }); + + it("deletes an account", async () => { + const res = await request(BASE_URL) + .delete("/api/accounts") + .set("Authorization", `Bearer ${token}`) + .send({ id: accountId }); + + expect(res.status).toBe(200); + expect(res.body.message).toBe("Account successfully deleted"); + }); + + it("rejects account creation without auth", async () => { + const res = await request(BASE_URL).post("/api/accounts").send({ + name: "Unauthorized", + type: "chequing", + }); + + expect(res.status).toBe(400); + }); + + it("validates account input", async () => { + const res = await request(BASE_URL) + .post("/api/accounts") + .set("Authorization", `Bearer ${token}`) + .send({ + name: "", // invalid + type: "chequing", + }); + + expect(res.status).toBe(400); + }); +}); diff --git a/app/server/services/ts/user/integration/setup.ts b/app/server/services/ts/user/integration/setup.ts new file mode 100644 index 0000000..5ba099a --- /dev/null +++ b/app/server/services/ts/user/integration/setup.ts @@ -0,0 +1,86 @@ +import request from "supertest"; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function createSignupBody(prefix: string) { + const safePrefix = prefix.replace(/[^\w]/g, ""); + const unique = `${safePrefix}${Date.now()}${Math.random().toString(36).slice(2, 8)}`; + + return { + username: unique, + email: `${unique}@example.com`, + first_name: "Test", + last_name: "User", + age: 30, + password: "123ABC!7", + }; +} + +export async function createAuthenticatedAccount( + baseUrl: string, + prefix: string, + accountOverrides?: { + name?: string; + type?: string; + balance?: number; + value?: number; + subtype?: string; + }, +) { + const deadline = Date.now() + 20000; + let lastFailure = "setup did not start"; + + while (Date.now() < deadline) { + const signupBody = createSignupBody(prefix); + + try { + const signup = await request(baseUrl) + .post("/api/signup") + .send(signupBody); + if (signup.status !== 201) { + lastFailure = `signup=${signup.status} ${JSON.stringify(signup.body)}`; + await sleep(500); + continue; + } + + const login = await request(baseUrl) + .post("/api/login") + .send({ email: signupBody.email, password: signupBody.password }); + if (login.status !== 200 || !login.body.token) { + lastFailure = `login=${login.status} ${JSON.stringify(login.body)}`; + await sleep(500); + continue; + } + + const token = login.body.token as string; + const account = await request(baseUrl) + .post("/api/accounts") + .set("Authorization", `Bearer ${token}`) + .send({ + name: accountOverrides?.name ?? "Chequing", + type: accountOverrides?.type ?? "chequing", + balance: accountOverrides?.balance ?? 1000, + value: accountOverrides?.value ?? 1000, + subtype: accountOverrides?.subtype ?? "na", + }); + if (account.status !== 200 || !account.body.id) { + lastFailure = `account=${account.status} ${JSON.stringify(account.body)}`; + await sleep(500); + continue; + } + + return { + token, + accountId: account.body.id as number, + }; + } catch (error) { + lastFailure = + error instanceof Error ? error.message : "unknown setup error"; + await sleep(500); + } + } + + throw new Error(`Timed out creating authenticated account: ${lastFailure}`); +} diff --git a/app/server/services/ts/user/integration/transaction.test.ts b/app/server/services/ts/user/integration/transaction.test.ts new file mode 100644 index 0000000..9756dba --- /dev/null +++ b/app/server/services/ts/user/integration/transaction.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import request from "supertest"; +import { createAuthenticatedAccount } from "./setup"; + +const BASE_URL = process.env.API_GATEWAY_ADDR; + +let token: string; +let accountId: number; + +beforeAll(async () => { + const setup = await createAuthenticatedAccount(BASE_URL, "transactions", { + name: "all my moola", + type: "chequing", + balance: 1000, + value: 1000, + subtype: "TFSA", + }); + + token = setup.token; + accountId = setup.accountId; +}, 30000); + +describe("Transactions Integration (Docker)", () => { + it("creates a transaction", async () => { + const res = await request(BASE_URL) + .post("/api/transactions") + .set("Authorization", `Bearer ${token}`) + .send({ + financialAccount_id: accountId, + amount: 50, + description: "Groceries", + sender: "Me", + recipient: "Store", + date: "2024-01-01T12:00:00", + category: "Food", + }); + + expect(res.status).toBe(200); + expect(res.body.id).toBeDefined(); + }); + + it("lists transactions for an account", async () => { + // Create a transaction first + await request(BASE_URL) + .post("/api/transactions") + .set("Authorization", `Bearer ${token}`) + .send({ + financialAccount_id: accountId, + amount: 20, + description: "Coffee", + sender: "Me", + recipient: "Cafe", + date: "2024-01-02T10:00:00", + category: "Food", + }); + + const res = await request(BASE_URL) + .get(`/api/transactions?financialAccount_id=${accountId}`) + .set("Authorization", `Bearer ${token}`); + + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + }); + + it("updates a transaction", async () => { + // Create a transaction + const create = await request(BASE_URL) + .post("/api/transactions") + .set("Authorization", `Bearer ${token}`) + .send({ + financialAccount_id: accountId, + amount: 10, + description: "Snack", + sender: "Me", + recipient: "Vending", + date: "2024-01-03T09:00:00", + category: "Food", + }); + + const id = create.body.id; + + const update = await request(BASE_URL) + .put("/api/transactions") + .set("Authorization", `Bearer ${token}`) + .send({ + id, + financialAccount_id: accountId, + amount: 15, + description: "Snack Updated", + sender: "Me", + recipient: "Vending", + date: "2024-01-03T09:30:00", + }); + + expect(update.status).toBe(200); + expect(update.body.message).toBe("Transaction successfully updated"); + }); + + it("deletes a transaction", async () => { + // Create a transaction + const create = await request(BASE_URL) + .post("/api/transactions") + .set("Authorization", `Bearer ${token}`) + .send({ + financialAccount_id: accountId, + amount: 100, + description: "Test", + sender: "Me", + recipient: "Someone", + date: "2024-01-04T12:00:00", + category: "Misc", + }); + + const id = create.body.id; + + const del = await request(BASE_URL) + .delete("/api/transactions") + .set("Authorization", `Bearer ${token}`) + .send({ id, financialAccount_id: accountId }); + + expect(del.status).toBe(200); + expect(del.body.message).toBe("Transaction deleted"); + }); + + it("imports CSV transactions", async () => { + const res = await request(BASE_URL) + .post("/api/transactions/csvTransaction") + .set("Authorization", `Bearer ${token}`) + .send({ + financialAccount_id: accountId, + transactions: [ + { + amount: 12.5, + description: "Lunch", + sender: "Me", + recipient: "Restaurant", + date: "2024-01-05T13:00:00", + category: "Food", + errors: [], + }, + { + amount: null, + description: null, + date: null, + errors: ["Missing amount"], + }, + ], + }); + + expect(res.status).toBe(200); + expect(res.body.inserted).toBe(1); + expect(res.body.skipped).toBe(1); + expect(res.body.transactions.length).toBe(1); + }); + + it("rejects unauthorized access", async () => { + const res = await request(BASE_URL).post("/api/transactions").send({ + financialAccount_id: accountId, + amount: 10, + date: "2024-01-01T12:00:00", + }); + + expect(res.status).toBe(401); + }); +}); diff --git a/app/server/services/ts/user/package-lock.json b/app/server/services/ts/user/package-lock.json new file mode 100644 index 0000000..3f9019d --- /dev/null +++ b/app/server/services/ts/user/package-lock.json @@ -0,0 +1,4554 @@ +{ + "name": "user-service", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "user-service", + "dependencies": { + "@mysql/xdevapi": "^8.0.35", + "axios": "^1.13.6", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mysql2": "^3.15.3", + "vitest": "^4.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/bun": "latest", + "@types/express": "^5.0.6", + "@types/node": "^25.1.0", + "@types/supertest": "^7.2.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "globals": "^17.3.0", + "jiti": "^2.6.1", + "supertest": "^7.2.2", + "typescript-eslint": "^8.54.0" + }, + "peerDependencies": { + "typescript": "^5.9.3" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@mysql/xdevapi": { + "version": "8.0.35", + "license": "GPL-2.0", + "dependencies": { + "google-protobuf": "3.19.4", + "lossless-json": "2.0.1", + "parsimmon": "1.18.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bun": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.11.tgz", + "integrity": "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.11" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.0", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.1", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.1", + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.1", + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.1", + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.1", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.1", + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.1", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.6", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/bun-types": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", + "integrity": "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-protobuf": { + "version": "3.19.4", + "license": "BSD-3-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lossless-json": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.18.2", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parsimmon": { + "version": "1.18.1", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.6", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.1", + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.1", + "@vitest/mocker": "4.1.1", + "@vitest/pretty-format": "4.1.1", + "@vitest/runner": "4.1.1", + "@vitest/snapshot": "4.1.1", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.1", + "@vitest/browser-preview": "4.1.1", + "@vitest/browser-webdriverio": "4.1.1", + "@vitest/ui": "4.1.1", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/app/server/services/ts/user/package.json b/app/server/services/ts/user/package.json index e407a4d..4c7ec47 100644 --- a/app/server/services/ts/user/package.json +++ b/app/server/services/ts/user/package.json @@ -9,12 +9,14 @@ "@types/bun": "latest", "@types/express": "^5.0.6", "@types/node": "^25.1.0", + "@types/supertest": "^7.2.0", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", "eslint": "^9.39.2", "eslint-plugin-react": "^7.37.5", "globals": "^17.3.0", "jiti": "^2.6.1", + "supertest": "^7.2.2", "typescript-eslint": "^8.54.0" }, "peerDependencies": { @@ -31,6 +33,6 @@ "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mysql2": "^3.15.3", - "vitest": "^4.0.18" + "vitest": "^4.1.1" } } diff --git a/app/server/services/ts/user/src/handleJWT.ts b/app/server/services/ts/user/src/handleJWT.ts index 6b0ec12..c150c66 100644 --- a/app/server/services/ts/user/src/handleJWT.ts +++ b/app/server/services/ts/user/src/handleJWT.ts @@ -4,7 +4,6 @@ const JWT_SECRET = process.env.JWT_SECRET; export const authenticateJWT = (req: Request) => { const authHeader = req.headers.authorization; - //console.log(req.headers); if (!authHeader) { throw new Error("Authorization header missing"); } @@ -15,7 +14,13 @@ export const authenticateJWT = (req: Request) => { } const token = parts[1]; //add as any if it doesn't work - const decoded = jwt.verify(token, JWT_SECRET); + let decoded; + try { + decoded = jwt.verify(token, JWT_SECRET); + } catch { + throw new Error("Could not verify given JWT."); + } + const userId = decoded.sub; //console.log("user id: " + userId); if (!userId) { diff --git a/app/server/services/ts/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts index 8b42f80..316483e 100644 --- a/app/server/services/ts/user/src/routes/account.ts +++ b/app/server/services/ts/user/src/routes/account.ts @@ -20,6 +20,22 @@ accountsRouter.post("/", async (req: Request, res: Response) => { const { name, type, balance, value, subtype } = req.body; const last_updated = new Date(); + if (!name || name.trim() === "") { + return res.status(400).json({ error: "Name is required" }); + } + + if (!type || typeof type !== "string") { + return res.status(400).json({ error: "Type is required" }); + } + + if (balance === undefined || balance === null || isNaN(balance)) { + return res.status(400).json({ error: "Balance is required" }); + } + + if (value === undefined || value === null || isNaN(value)) { + return res.status(400).json({ error: "Value is required" }); + } + let userId; try { diff --git a/app/server/services/ts/user/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts index 05bc8a3..26e7413 100644 --- a/app/server/services/ts/user/src/routes/transaction.ts +++ b/app/server/services/ts/user/src/routes/transaction.ts @@ -337,15 +337,13 @@ async function checkUserId( try { //Checks if the profile has an account with that id const [rows] = await db.query( - `SELECT 1 FROM profile_financialAccount + `SELECT * FROM profile_financialAccount WHERE profile_id =? AND financialAccount_id =?`, [userId, accountId], ); console.log(rows.length); - if (rows.length > 0) { - result = true; - } + result = rows.length > 0; } catch (error) { console.error(error); } diff --git a/documentation/Acceptance/MarketAcceptance/MarketAcceptance.md b/documentation/Acceptance/MarketAcceptance/MarketAcceptance.md new file mode 100644 index 0000000..15727a6 --- /dev/null +++ b/documentation/Acceptance/MarketAcceptance/MarketAcceptance.md @@ -0,0 +1,61 @@ +## Acceptance Criteria +1: Search +Searching “AAPL” returns Apple stock. +Searching “USD/CAD” returns the forex pair. +Empty search returns no results. +Invalid symbols return an empty list. + +2: Market Data +GET /market/AAPL returns: + +- price +- daily change +- timestamp + +3: Historical Data +GET /market/=AAPL&range=1M returns: +an array of price points +no gaps in chart data +missing days filled with the last known value + +4: Pinning +POST /user/pins stores a pin. +GET /user/pins returns all pinned instruments. +Duplicate pins are rejected. + +5: Unpinning +DELETE /user/pins/:symbol removes the pin. + +6: Sync on Login +After login, pinned instruments appear on the dashboard automatically. + +## Acceptance Test Scenarios +1 — Search for an Instrument +Given the user is authenticated +When they search for “AAPL” +Then the system returns a list containing Apple stock + +2 — View Market Quote +Given the user selects “AAPL” +When they open the instrument page +Then the system shows current price and daily change + +3 — View Historical Chart +Given the user selects “AAPL” +When they choose the 1‑month range +Then the system displays a chart with normalized data + +4 — Pin an Instrument +Given the user is authenticated +When they pin “AAPL” +Then the pin appears in their profile + +5 — Sync Pins on Login +Given the user has pinned “AAPL” +When they log in +Then the dashboard shows “AAPL” + +6 — Unpin an Instrument +Given the user has pinned “AAPL” +When they unpin it +Then it is removed from their profile \ No newline at end of file diff --git a/documentation/Acceptance/UserInp/UserInpAcceptance.md b/documentation/Acceptance/UserInp/UserInpAcceptance.md new file mode 100644 index 0000000..852f4d4 --- /dev/null +++ b/documentation/Acceptance/UserInp/UserInpAcceptance.md @@ -0,0 +1,59 @@ +# Acceptance Criteria + +## 1: Manual Financial Entry +- Users can manually enter financial data such as incomes, expenses, investments, and individual transactions. +- Users can input transaction details including name, amount, date, and category. +- Manually entered data appears immediately in the user’s financial profile. + +## 2: CSV Import +- Users can upload a CSV bank statement for parsing. +- The system extracts transaction names, dates, amounts, and descriptions. +- Empty or malformed rows are ignored without breaking the import. +- Parsed transactions are displayed for user confirmation before saving. +- Invalid CSV formats return a clear error message. + +## 3: Financial Profiles +- Users can create financial profiles that aggregate all accounts, transactions, and categories. +- Profiles persist across sessions. +- Profiles update automatically when new transactions are added manually or via CSV. + +## 4: Edit Transactions +- Users can edit the category of any historical transaction. +- Users can edit the name of any historical transaction. +- Users can edit the financial value of any historical transaction. +- Edited transactions update all related summaries and totals. + +--- + +# Acceptance Test Scenarios + +## 1 — Manual Entry of Financial Data +Given the user is authenticated +When they manually enter a new income, expense, or investment +Then the system stores the entry +And it appears in their financial profile + +## 2 — CSV Import and Parsing +Given the user uploads a valid CSV bank statement +When the system parses the file +Then transactions are extracted and displayed for review +And invalid rows are ignored without stopping the import + + +## 3 — Editing Transaction Category +Given a transaction is incorrectly categorized +When the user selects a new category +Then the transaction is updated +And category totals reflect the change + +## 4 — Editing Transaction Name +Given a transaction name is unclear or incorrect +When the user edits the name +Then the updated name appears across all views + +## 5 — Editing Transaction Value +Given a transaction amount is wrong +When the user edits the value +Then the corrected value updates account balances and summaries + + diff --git a/documentation/sequence-diagrams/stockTracking/SeqDiagramStock.png b/documentation/sequence-diagrams/stockTracking/SeqDiagramStock.png new file mode 100644 index 0000000000000000000000000000000000000000..bebfcefe1a16ed100ba01b3e6bb4d7ef6a5f1835 GIT binary patch literal 44388 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 Ocb_=oe3;_sbL(H_yEf7Q literal 0 HcmV?d00001 From d320e018f17402d89cbb0de50837412d0dffcaa6 Mon Sep 17 00:00:00 2001 From: TebelR Date: Fri, 27 Mar 2026 11:48:19 -0500 Subject: [PATCH 188/204] Merging Goals into Dev after fixing merge conflicts --- .gitignore | 3 + app/client/src/api/GoalsAPI.ts | 47 + app/client/src/components/AppLayout.tsx | 148 +- app/client/src/components/GoalsPanel.tsx | 405 + app/client/src/types/goals.ts | 31 + app/server/docker-compose.test.yml | 19 + .../results.json | 1 + app/server/services/database/populator.py | 28 +- app/server/services/database/schema.sql | 16 +- .../python/analytics/src/dependencies.py | 15 +- .../python/analytics/src/logic/budget.py | 16 +- .../python/analytics/src/logic/budgetCalc.py | 132 +- .../python/analytics/src/logic/savings.py | 2 +- .../python/analytics/src/queries/budget.py | 37 +- .../python/analytics/test/conftest.py | 17 - .../python/analytics/test/test_budgetCalc.py | 518 +- .../analytics/test/test_budgetPerfCalc.py | 69 + .../analytics/test/test_budget_integration.py | 12 +- .../analytics/test/test_dependencies.py | 68 +- .../python/analytics/test/test_savings.py | 29 +- .../services/ts/api-gateway/src/index.ts | 80 +- app/server/services/ts/bun.lock | 351 +- app/server/services/ts/common/expressUtils.ts | 2 +- app/server/services/ts/package.json | 7 +- .../ts/user/Dockerfile.integration.test | 3 +- .../services/ts/user/Dockerfile.mutation.test | 10 + app/server/services/ts/user/bun.lock | 342 +- .../ts/user/integration/goals.test.ts | 238 + .../services/ts/user/integration/setup.ts | 83 + app/server/services/ts/user/package.json | 10 +- .../ts/user/reports/mutation/mutation.html | 11350 ++++++++++++++++ app/server/services/ts/user/src/index.ts | 690 +- .../services/ts/user/src/logic/goals.ts | 260 + .../services/ts/user/src/queries/goals.ts | 201 + .../ts/user/src/queries/transactions.ts | 93 + .../services/ts/user/src/types/Goals.ts | 34 + app/server/services/ts/user/stryker.conf.json | 9 + .../services/ts/user/strykerNODE.conf.json | 12 + .../services/ts/user/test/dates.test.ts | 1 - .../ts/user/test/factories/goal.factory.ts | 56 + .../ts/user/test/goals.period.test.ts | 64 + .../services/ts/user/test/goals.test.ts | 496 + .../services/ts/user/vitest.config.stryker.ts | 13 + documentation/Finus_Test_Plan.md | 153 +- documentation/acceptance tests/goals.md | 21 + .../goals/Goals Sequence Diagram.png | Bin 0 -> 101935 bytes meeting-minutes/Meeting_10_Mar-23.md | 85 + meeting-minutes/Meeting_8_Mar-04.md | 40 + meeting-minutes/Meeting_9_Mar-08.md | 37 + 49 files changed, 15585 insertions(+), 769 deletions(-) create mode 100644 app/client/src/api/GoalsAPI.ts create mode 100644 app/client/src/components/GoalsPanel.tsx create mode 100644 app/client/src/types/goals.ts create mode 100644 app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json create mode 100644 app/server/services/ts/user/Dockerfile.mutation.test create mode 100644 app/server/services/ts/user/integration/goals.test.ts create mode 100644 app/server/services/ts/user/reports/mutation/mutation.html create mode 100644 app/server/services/ts/user/src/logic/goals.ts create mode 100644 app/server/services/ts/user/src/queries/goals.ts create mode 100644 app/server/services/ts/user/src/types/Goals.ts create mode 100644 app/server/services/ts/user/stryker.conf.json create mode 100644 app/server/services/ts/user/strykerNODE.conf.json create mode 100644 app/server/services/ts/user/test/factories/goal.factory.ts create mode 100644 app/server/services/ts/user/test/goals.period.test.ts create mode 100644 app/server/services/ts/user/test/goals.test.ts create mode 100644 app/server/services/ts/user/vitest.config.stryker.ts create mode 100644 documentation/acceptance tests/goals.md create mode 100644 documentation/sequence-diagrams/goals/Goals Sequence Diagram.png create mode 100644 meeting-minutes/Meeting_10_Mar-23.md create mode 100644 meeting-minutes/Meeting_8_Mar-04.md create mode 100644 meeting-minutes/Meeting_9_Mar-08.md diff --git a/.gitignore b/.gitignore index 47d3e1e..e2e8776 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,6 @@ marimo/_static/ marimo/_lsp/ __marimo__/ app/server/services/api-gateway/bun.lock + +# stryker files, these are ran with node and break git error checking - mutation testing for JS +.stryker-tmp \ No newline at end of file diff --git a/app/client/src/api/GoalsAPI.ts b/app/client/src/api/GoalsAPI.ts new file mode 100644 index 0000000..c9cbb9a --- /dev/null +++ b/app/client/src/api/GoalsAPI.ts @@ -0,0 +1,47 @@ +import type { Goal } from "../types/goals.ts"; +import { instance } from "./config"; + +async function fetchGoals(): Promise { + console.log("fetching goals"); + const response = await instance.get("/goals"); + if (response.status !== 200) { + throw new Error(`Failed to fetch goals data: ${response.statusText}`); + } + console.log("returning a list of goals: ", response.data); + return response.data; +} + +async function createGoal(goal: Partial): Promise { + console.log("creating goal: ", goal); + const response = await instance.post("/goals", goal); + if (response.status !== 201) { + // 201 Created is standard for POST + throw new Error(`Failed to create goal: ${response.statusText}`); + } + console.log("got response from create goal: ", response.data); + return response.data; +} + +async function updateGoal( + goalId: string, + updates: Partial, +): Promise { + const response = await instance.patch(`/goals?gid=${goalId}`, updates); + if (response.status !== 200) { + throw new Error(`Failed to edit goal: ${response.statusText}`); + } + + // console.log("updated goal: ", response.data); + return response.data; +} + +async function deleteGoal(goalId: string): Promise { + // console.log("deleting goal: ", goalId); + const response = await instance.delete(`/goals?gid=${goalId}`); + if (response.status !== 204) { + // 204 No Content is standard for DELETE + throw new Error(`Failed to delete goal: ${response.statusText}`); + } +} + +export { fetchGoals, createGoal, updateGoal, deleteGoal }; diff --git a/app/client/src/components/AppLayout.tsx b/app/client/src/components/AppLayout.tsx index 92620d9..67a647f 100644 --- a/app/client/src/components/AppLayout.tsx +++ b/app/client/src/components/AppLayout.tsx @@ -1,13 +1,94 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Outlet } from "react-router-dom"; -import { IoReorderThreeSharp } from "react-icons/io5"; +import { IoCheckmarkCircleOutline, IoReorderThreeSharp } from "react-icons/io5"; import NavBar from "./NavBar"; import { SIDEBAR_WIDTH } from "../utils/constants"; +import GoalsPanel from "./GoalsPanel"; +import type { Goal } from "../types/goals.ts"; +import { + fetchGoals, + createGoal, + updateGoal, + deleteGoal, +} from "../api/GoalsAPI.ts"; +import LoadingSpinner from "@/components/LoadingSpinner"; + type AppLayoutProps = { onLogout: () => void; }; export default function AppLayout({ onLogout }: AppLayoutProps) { const [isNavBarOpen, setIsNavBarOpen] = useState(false); + const [isGoalsPanelOpen, setIsGoalsPanelOpen] = useState(false); + const closeTimeoutRef = useRef(null); + const [goals, setGoals] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + loadGoals(); + }, []); + + const loadGoals = async () => { + try { + setIsLoading(true); + const data = await fetchGoals(); + setGoals(data); + } catch (error) { + console.error("Failed to load goals:", error); + } finally { + setIsLoading(false); + } + }; + + const handleAddGoal = async () => { + // need a modal or a form for this placeholder + const newGoal: Partial = { + type: "reduce_spending", + name: "New Goal", + category: "Unknown", + target: 100, + current_amount: 0, + period: "m", + }; + + try { + const created = await createGoal(newGoal); + setGoals([...goals, created]); + } catch (error) { + console.error("Failed to create goal:", error); + } + }; + + const handleEditGoal = async (goalId: string, updates: Partial) => { + try { + const updated = await updateGoal(goalId, updates); + // console.log("Updated goal:", updated); + setGoals(goals.map((g) => (g.id === goalId ? updated : g))); + } catch (error) { + console.error("Failed to update goal:", error); + } + }; + + const handleDeleteGoal = async (goalId: string) => { + try { + await deleteGoal(goalId); + setGoals(goals.filter((g) => g.id !== goalId)); + } catch (error) { + console.error("Failed to delete goal:", error); + } + }; + + const handleOpenPanel = () => { + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + setIsGoalsPanelOpen(true); + }; + + const handleClosePanel = () => { + closeTimeoutRef.current = setTimeout(() => { + setIsGoalsPanelOpen(false); + }, 200); //200ms delay before the goals panel closes + }; function toggleSidebar() { setIsNavBarOpen((prev) => !prev); @@ -44,6 +125,69 @@ export default function AppLayout({ onLogout }: AppLayoutProps) { " /> + +
+ {isLoading ? ( +
+ +
+ ) : ( + + )} +
{isNavBarOpen && ( ; + +type EditValuesAction = + | { + type: "INIT_GOAL"; + payload: { goalId: string; goal: Goal }; + } + | { + type: "UPDATE_FIELD"; + payload: { + goalId: string; + field: string; + value: string | number; + }; + }; + +function editValuesReducer( + state: EditValuesState, + action: EditValuesAction, +): EditValuesState { + switch (action.type) { + case "INIT_GOAL": { + const { goalId, goal } = action.payload; + if (state[goalId]) return state; + + const baseValues = { + name: goal.name || "", + category: goal.category || "", + target: goal.target || 0, + }; + + if (goal.type === "reduce_spending") { + return { + ...state, + [goalId]: { + ...baseValues, + type: "reduce_spending", + period: goal.period || "m", + }, + }; + } else { + // type === "save" + return { + ...state, + [goalId]: { + ...baseValues, + type: "save", + period: "m", + }, + }; + } + } + + case "UPDATE_FIELD": { + const { goalId, field, value } = action.payload; + const current = state[goalId]; + if (!current) return state; + + // Handle field updates with type safety + if (field === "type") { + // When type changes, we need to restructure the edit value + if (value === "reduce_spending") { + return { + ...state, + [goalId]: { + name: current.name, + category: current.category, + target: current.target, + type: "reduce_spending", + period: "m", // default period + } as EditValueReduceSpending, + }; + } else { + return { + ...state, + [goalId]: { + name: current.name, + category: current.category, + target: current.target, + type: "save", + period: "m", + } as EditValueSave, + }; + } + } + + // For other fields, preserve the type + if (field === "period" && current.type === "reduce_spending") { + return { + ...state, + [goalId]: { + ...current, + period: value as "m" | "w", + }, + }; + } + + // Generic field update + return { + ...state, + [goalId]: { + ...current, + [field]: value, + }, + }; + } + + default: + return state; + } +} + +function GoalsPanel({ + goals, + onAddGoal, + onEditGoal, + onDeleteGoal, + maxGoals = 5, +}: GoalsPanelProps) { + const [expandedGoalId, setExpandedGoalId] = useState(null); + const [editValues, dispatch] = useReducer(editValuesReducer, {}); + + const initializedGoals = useRef>(new Set()); + + //initialize edit values when a goal expands + useEffect(() => { + if (expandedGoalId) { + const goal = goals.find((g: Goal) => g.id === expandedGoalId); + if (goal && !initializedGoals.current.has(expandedGoalId)) { + initializedGoals.current.add(expandedGoalId); + dispatch({ + type: "INIT_GOAL", + payload: { goalId: expandedGoalId, goal }, + }); + } + } + }, [expandedGoalId, goals]); + + //helper to format currency + const formatAmount = (amount: number): string => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "CAD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(amount); + }; + + //helper to get goal type, only the reduce_spending type has a monthly or a weekly period, savings are eternal + const getGoalDescription = (goal: Goal): string => { + if (goal.type === "reduce_spending") { + return `Spending limit • ${goal.period === "m" ? "Monthly" : "Weekly"}`; + } else { + return `Savings Goal`; + } + }; + + //toggle expanded view of a goal + const handleGoalClick = (goalId: string) => { + setExpandedGoalId(expandedGoalId === goalId ? null : goalId); + }; + + //helper to handle field changes + const handleFieldChange = ( + goalId: string, + field: string, + value: string | number, + ) => { + dispatch({ type: "UPDATE_FIELD", payload: { goalId, field, value } }); + }; + + const handleApplyEdits = (goalId: string) => { + const updates = editValues[goalId]; + if (updates) { + onEditGoal(goalId, updates); + setExpandedGoalId(null); + } + }; + + //saves the edits in state even of the panel closes + const hasUnsavedChanges = (goalId: string): boolean => { + const goal = goals.find((g: Goal) => g.id === goalId); + const edits = editValues[goalId]; + if (!goal || !edits) return false; + + return ( + goal.name !== edits.name || + goal.category !== edits.category || + goal.type !== edits.type || + goal.period !== edits.period || + goal.target !== edits.target + ); + }; + + return ( +
+
+

Your Goals

+ + {goals.length} / {maxGoals} + +
+ +
+ {goals.map((goal: Goal) => { + const progress = goal.progress_percentage; + const isExpanded = expandedGoalId === goal.id; + const currentEdits = editValues[goal.id]; + const hasChanges = hasUnsavedChanges(goal.id); + + const progressBarColor = + goal.type === "reduce_spending" ? "bg-red-500" : "bg-green-500"; + + return ( +
handleGoalClick(goal.id)} + > +
+ + {goal.name} - {goal.category} + + + {formatAmount(goal.current_amount)} /{" "} + {formatAmount(goal.target)} + +
+ +
+
+
+ +

+ {getGoalDescription(goal)} +

+ + {/* expanded details with editable fields */} + {isExpanded && currentEdits && ( +
+ {/* goal name */} +
+ Goal Name: + + handleFieldChange(goal.id, "name", e.target.value) + } + className="bg-black/50 border border-green-500/30 rounded px-2 py-1 text-sm text-white w-48" + onClick={(e) => e.stopPropagation()} + /> +
+ + {/* category */} +
+ Category: + + handleFieldChange(goal.id, "category", e.target.value) + } + className="bg-black/50 border border-green-500/30 rounded px-2 py-1 text-sm text-white w-48" + onClick={(e) => e.stopPropagation()} + /> +
+ + {/* goal type */} +
+ Goal Type: + +
+ + {/* period (only for reduce_spending) */} + {currentEdits.type === "reduce_spending" && ( +
+ Period: + +
+ )} + + {/* target amount */} +
+ + Target Amount: + + + handleFieldChange( + goal.id, + "target", + parseFloat(e.target.value) || 0, + ) + } + className="bg-black/50 border border-green-500/30 rounded px-2 py-1 text-sm text-white w-32 text-right" + onClick={(e) => e.stopPropagation()} + /> +
+ + {/* action buttons */} +
+ + +
+
+ )} +
+ ); + })} + + {/* add goal button */} + {goals.length < maxGoals && ( + + )} +
+
+ ); +} + +export default GoalsPanel; diff --git a/app/client/src/types/goals.ts b/app/client/src/types/goals.ts new file mode 100644 index 0000000..5e25225 --- /dev/null +++ b/app/client/src/types/goals.ts @@ -0,0 +1,31 @@ +export type GoalType = "reduce_spending" | "save"; // just two for simplicity, but can expand + +export interface BaseGoal { + id: string; + name: string; + type: GoalType; + category: string; + target: number; + period: "m" | "w"; //"na" for savings + progress_percentage: number; + current_amount: number; //this is the amount that has been saved or spent depending on goal type. This is aggregated on the server, and is not actually stored in DB +} + +export interface SpendingLimitGoal extends BaseGoal { + type: "reduce_spending"; + period: "m" | "w"; +} + +export interface SavingsTargetGoal extends BaseGoal { + type: "save"; +} + +export type Goal = SpendingLimitGoal | SavingsTargetGoal; + +export interface GoalsPanelProps { + goals: Goal[]; + onAddGoal: () => void; + onEditGoal: (goalId: string, updates: Partial) => void; + onDeleteGoal: (goalId: string) => void; + maxGoals?: number; +} diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 7ac3567..096f10c 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -19,6 +19,25 @@ services: context: services/python/analytics dockerfile: Dockerfile.test + # user-integration-test:#uncomment to run integration tests locally as it likely does not work properly in CI/CD + # build: + # context: ./services/ts + # dockerfile: user/Dockerfile.integration.test + # env_file: + # - ./default_container.env + # environment: + # - JWT_SECRET + + # depends_on: + # database: + # condition: service_healthy + # api-gateway: + # condition: service_started + + # user-mutation-test: + # build: + # context: ./services/ts + # dockerfile: user/Dockerfile.mutation.test market-test: build: context: services/python/market diff --git a/app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json new file mode 100644 index 0000000..c418db7 --- /dev/null +++ b/app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json @@ -0,0 +1 @@ +{"version":"4.1.1","results":[[":services/ts/user/test/logic_snapshot.test.ts",{"duration":24.460399999999993,"failed":true}],[":services/ts/user/test/dates.test.ts",{"duration":28.86179999999996,"failed":true}],[":services/ts/user/test/logic_expenses.test.ts",{"duration":14.341799999999978,"failed":false}],[":services/ts/auth/test/logic.test.ts",{"duration":0,"failed":true}],[":services/ts/user/test/logic_transactions.test.ts",{"duration":8.949200000000019,"failed":false}],[":services/ts/auth/test/parsing.test.ts",{"duration":0,"failed":true}],[":services/ts/auth/test/validation.test.ts",{"duration":6.108399999999989,"failed":false}]]} \ No newline at end of file diff --git a/app/server/services/database/populator.py b/app/server/services/database/populator.py index 88e5629..57292b0 100644 --- a/app/server/services/database/populator.py +++ b/app/server/services/database/populator.py @@ -9,12 +9,9 @@ import mysql.connector from mysql.connector import Error import random -import hashlib -import os from datetime import datetime, timedelta import argparse import bcrypt -import subprocess def get_db_connection(): @@ -43,7 +40,7 @@ def get_db_connection(): FINANCIAL_ACCOUNT_TYPES = ['chequing', 'savings', 'credit_card', 'investment'] FINANCIAL_ACCOUNT_SUBTYPES = ['RRSP', 'TFSA', 'FHSA', 'RESP', 'RDSP', 'loan', 'na'] INVESTMENT_TYPES = ['stocks', 'bonds', 'mutual funds', 'ETFs'] -GOAL_TYPES = ['money', 'debt'] +GOAL_TYPES = ['save', 'reduce_spending'] FIRST_NAMES = ['John', 'Jane', 'Alex', 'Emily', 'Michael', 'Sarah', 'David', 'Laura'] LAST_NAMES = ['Smith', 'Finus', 'Williams', 'Brown', 'Jones'] GOAL_NAMES = ['Emergency Fund', 'New Car', 'House Down Payment', 'Vacation', 'Retirement', 'Pay off Credit Card'] @@ -67,14 +64,13 @@ def create_goals(cursor, profile_ids): for _ in range(random.randint(1, 3)): name = random.choice(GOAL_NAMES) goal_type = random.choice(GOAL_TYPES) - objective = random.choice(GOAL_OBJECTIVES) - description = f"Goal: {objective}" - deadline = datetime.now() + timedelta(days=random.randint(30, 365*3)) # 1 month to 3 years - + category = random.choice(TRANSACTION_CATEGORIES) + target = random.randint(100, 50000) + period = random.choice(['w', 'm']) cursor.execute(""" - INSERT INTO finus.goal (name, type, objective, description, deadline) - VALUES (%s, %s, %s, %s, %s) - """, (name, goal_type, objective, description, deadline)) + INSERT INTO finus.goal (name, type, category, target, period) + VALUES (%s, %s, %s, %s , %s) + """, (name, goal_type, category, target , period)) goal_id = cursor.lastrowid @@ -355,11 +351,11 @@ def populate_lookup_tables(cursor): (inv_type,) ) - for goal_type in GOAL_TYPES: - cursor.execute( - "INSERT IGNORE INTO finus.goalType (type) VALUES (%s)", - (goal_type,) - ) + # for goal_type in GOAL_TYPES: + # cursor.execute( + # "INSERT IGNORE INTO finus.goalType (type) VALUES (%s)", + # (goal_type,) + # ) diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql index 900a284..b5895b4 100644 --- a/app/server/services/database/schema.sql +++ b/app/server/services/database/schema.sql @@ -34,21 +34,15 @@ CREATE TABLE finus.finusAccount_profile ( FOREIGN KEY (account_id) REFERENCES finus.finusAccount(id) ON DELETE CASCADE -- this used to mention financialAccount, but should be mentioning finusAccount ); -CREATE TABLE finus.goalType ( - type VARCHAR(50) NOT NULL, - PRIMARY KEY (type) -); CREATE TABLE finus.goal ( id INTEGER NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL, - objective VARCHAR(500) NOT NULL, - description VARCHAR(500), - deadline DATETIME, - PRIMARY KEY (id), - FOREIGN KEY (type) REFERENCES finus.goalType(type) ON DELETE CASCADE - + category VARCHAR(50), + period VARCHAR(1) NOT NULL, + target DECIMAL(12,2) NOT NULL, -- this is the monetary amount that we are trying to reach, there is no current amount to track, as that is just recalculated from the transactions + PRIMARY KEY (id) ); CREATE TABLE finus.profile_goal ( @@ -164,4 +158,4 @@ INSERT INTO finus.financialAccountType (type) VALUES ('chequing'), ('savings' INSERT INTO finus.financialAccountSubtype (subtype) VALUES ('RRSP'), ('TFSA'), ('FHSA'), ('RESP'), ('RDSP'), ('loan'), ('na'); -- loan is used for credit_card accounts that are for loans like mortgage and etc, this is used to track debt INSERT INTO finus.investmentType (type) VALUES ('fixedInterest'), ('stock'); #These have to match table names -INSERT INTO finus.goalType (type) VALUES ('money'), ('debt'); + diff --git a/app/server/services/python/analytics/src/dependencies.py b/app/server/services/python/analytics/src/dependencies.py index ae26603..ebb1ac0 100644 --- a/app/server/services/python/analytics/src/dependencies.py +++ b/app/server/services/python/analytics/src/dependencies.py @@ -32,16 +32,7 @@ async def get_current_user(authorization: Optional[str] = Header(None)): payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id = payload.get('sub') - if not user_id: - raise HTTPException(status_code=401, detail="User ID not found in token") - return int(user_id) - - except jwt.ExpiredSignatureError: - raise HTTPException(status_code=401, detail="Token expired") - except jwt.InvalidTokenError: - raise HTTPException(status_code=401, detail="Invalid token") - except ValueError: - raise HTTPException(status_code=401, detail="Invalid authorization header format") - except TypeError as e: - raise HTTPException(status_code=401, detail=str(e)) \ No newline at end of file + + except Exception as e: + raise HTTPException(status_code=401, detail=str(e))#general exception for simpler unit \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/budget.py b/app/server/services/python/analytics/src/logic/budget.py index d34c237..cea8f34 100644 --- a/app/server/services/python/analytics/src/logic/budget.py +++ b/app/server/services/python/analytics/src/logic/budget.py @@ -1,7 +1,7 @@ from src.logic.budgetCalc import BudgetCalculator from src.logic.budgetPerfCalc import BudgetPerformanceCalculator from src.models.schemas import BudgetResponse -from src.queries.budget import get_user_transactions_with_connection +from src.queries.budget import get_user_transactions_with_connection, get_user_goals from src.utils.trans_cat_classifier import CategoryClassifier import pandas as pd @@ -14,20 +14,30 @@ # Find the top 10 most impactful categories and calculate a budget for each #At the moment, this uses a 50/30/20 rule, where categories are classified as needs, wants, savings and then a proportional budget is calculated # Monthly budget is generated based on the average monthly income, but then it can be divided or multiplied to match the specified period (week is divided by 4.33, year multiplied by 12) + +#More technical detail: +#Pools of money are created for needs, wants and savings based on income per month - the budget is created on a monthly basis and can be rescaled for weeks or years later. +# Each savings goal adds a flat 5% amount increase to the savings pool, money is taken away from wants pool and added to savings +# Each expenditure reduction goal adds a progressive % to savings pool based on how close the expenditure is to the limit, and takes away this money from wants pool - adding it to savings +# Needs pool is untouched always +# The most impactful categories are then used to calculate a budget for each category by drawing out of the pools +# Categories are categorized into needs, wants, savings and then a proportional budget is calculated + def generate_budget(period: str, user_id: int) -> BudgetResponse: end_date = pd.Timestamp.today() start_date = end_date - pd.Timedelta(days=365) - transactions = get_user_transactions_with_connection( user_id, start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d') ) + goals = get_user_goals(user_id) + income = [t for t in transactions if t['amount'] > 0] avg_monthly_income = sum(t['amount'] for t in income) / 12 - return _default_calculator.generate_budget(transactions, period, avg_monthly_income) + return _default_calculator.generate_budget(transactions, period, avg_monthly_income, goals) #Calculate the performance against a proposed user budget diff --git a/app/server/services/python/analytics/src/logic/budgetCalc.py b/app/server/services/python/analytics/src/logic/budgetCalc.py index afe6367..70a49aa 100644 --- a/app/server/services/python/analytics/src/logic/budgetCalc.py +++ b/app/server/services/python/analytics/src/logic/budgetCalc.py @@ -1,18 +1,23 @@ -from typing import List, Dict, Tuple +from typing import List, Dict, Optional, Tuple import pandas as pd from src.models.schemas import BudgetCategory, BudgetResponse from src.utils.trans_cat_classifier import CategoryClassifier + class BudgetCalculator: def __init__(self, classifier: CategoryClassifier): self.classifier = classifier self.WEEKS_PER_MONTH = 4.33 + + def separate_income_expenses(self, transactions: List[Dict]) -> Tuple[List[Dict], List[Dict]]: income = [t for t in transactions if t['amount'] > 0] expenses = [t for t in transactions if t['amount'] < 0] return income, expenses + + def calculate_monthly_averages(self, expenses: List[Dict]) -> Dict[str, float]: expense_by_category = {} for t in expenses: @@ -22,9 +27,13 @@ def calculate_monthly_averages(self, expenses: List[Dict]) -> Dict[str, float]: return {cat: total / 12 for cat, total in expense_by_category.items()} + + def get_top_categories(self, monthly_avg: Dict[str, float], n: int = 10) -> List[Tuple[str, float]]: return sorted(monthly_avg.items(), key=lambda x: x[1], reverse=True)[:n] + + def calculate_budget_pools(self, avg_monthly_income: float) -> Dict[str, float]: return { 'needs': avg_monthly_income * 0.5, @@ -32,6 +41,57 @@ def calculate_budget_pools(self, avg_monthly_income: float) -> Dict[str, float]: 'savings': avg_monthly_income * 0.2 } + + #This calculates the impact of goals on the budget - the goal type is important + # Pure savings goals incur a flat boost to savings + # Reduce spending goals reduce wants by 15% if the spending is over the limit, this penalty becomes smaller the further away the spending is from the limit + def calculate_goal_impact( + self, + goals: List[Dict], + avg_monthly_income: float, + monthly_avg_by_category: Dict[str, float] + ) -> Tuple[float, float, List[str]]: + savings_boost = 0.0 + wants_reduction = 0.0 + categories_to_include = set() + + for goal in goals: + category = goal.get('category') + goal_type = goal.get('type') + + if category: + categories_to_include.add(category) + + if goal_type == 'save': + #save goals add 5% boost to savings + boost_amount = avg_monthly_income * 0.05 + savings_boost += boost_amount + + elif goal_type == 'reduce_spending': + #get current spending for this category + current_spending = monthly_avg_by_category.get(category, 0) + target_amount = float(goal.get('target', 0)) + + if target_amount > 0: + percent_of_limit = (current_spending / target_amount) * 100 + + if percent_of_limit >= 100: + #already over limit: reduce wants by 7% + reduction = avg_monthly_income * 0.07 + wants_reduction = max(wants_reduction, reduction) + elif percent_of_limit >= 80: + #approaching limit: reduce wants by 5% + reduction = avg_monthly_income * 0.05 + wants_reduction = max(wants_reduction, reduction) + elif percent_of_limit >= 60: + #getting close: reduce wants by 3% + reduction = avg_monthly_income * 0.03 + wants_reduction = max(wants_reduction, reduction) + + return savings_boost, wants_reduction, list(categories_to_include) + + + def calculate_category_budget( self, category: str, @@ -39,8 +99,12 @@ def calculate_category_budget( category_type: str, monthly_avg_by_category: Dict[str, float], category_types: Dict[str, str], - pools: Dict[str, float] + pools: Dict[str, float], + goals: List[Dict] = None ) -> float: + goals = goals or [] + goal = next((g for g in goals if g.get('category') == category and g.get('type') == 'reduce_spending'), None) + if category_type == 'need': pool_total = pools['needs'] pool_actual = sum( @@ -53,11 +117,22 @@ def calculate_category_budget( monthly_avg_by_category.get(c, 0) for c, t in category_types.items() if t == 'want' ) - + + #apply spending limit goal if exists if pool_actual > 0: - return (avg_spent / pool_actual) * pool_total - return avg_spent * 0.9 # try to reduce spending by 10% if there is absolutely no income + recommended = (avg_spent / pool_actual) * pool_total + else: + recommended = avg_spent * 0.9 + + #apply spending limit goal if exists + if goal and goal.get('target', 0) > 0: + goal_target_monthly = float(goal['target']) + recommended = min(recommended, goal_target_monthly) + + return recommended# try to reduce spending by 10% if there is absolutely no income + + def scale_budget(self, amount: float, period: str) -> float: if period == 'w': return amount / self.WEEKS_PER_MONTH @@ -66,27 +141,68 @@ def scale_budget(self, amount: float, period: str) -> float: else: # 'y' return amount * 12 + + def generate_budget( self, transactions: List[Dict], period: str, - avg_monthly_income: float + avg_monthly_income: float, + goals: Optional[List[Dict]] = None ) -> BudgetResponse: - """Generate budget from transactions.""" + goals = goals or [] income, expenses = self.separate_income_expenses(transactions) monthly_avg = self.calculate_monthly_averages(expenses) + monthly_avg_savings = self.calculate_monthly_averages(income) pools = self.calculate_budget_pools(avg_monthly_income) + savings_boost, wants_reduction, goal_categories = self.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg + ) + + if savings_boost > 0: + #transfer from wants to savings + pools['wants'] = max(0, pools['wants'] - savings_boost) + pools['savings'] = pools['savings'] + savings_boost + + if wants_reduction > 0: + #reduce wants and increase savings + pools['wants'] = max(0, pools['wants'] - wants_reduction) + pools['savings'] = pools['savings'] + wants_reduction + category_types = self.classifier.classify_many(set(monthly_avg.keys())) + + #ensure goal categories are included in category_types + for goal_cat in goal_categories: + if goal_cat not in category_types: + #default to 'want' if not classified + category_types[goal_cat] = 'want' + top_categories = self.get_top_categories(monthly_avg) + top_category_names = {cat for cat, _ in top_categories} + #add any missing goal categories to top_categories + for goal_cat in goal_categories: + if goal_cat not in top_category_names and goal_cat in monthly_avg:#some goals may have expenses in their categories + top_categories.append((goal_cat, monthly_avg[goal_cat])) + if goal_cat not in top_category_names and goal_cat in monthly_avg_savings:#some goals are just savings goals and their categories have no expenses, so they won't be in monthly expenses + top_categories.append((goal_cat, monthly_avg_savings[goal_cat])) + if goal_cat not in top_category_names:#lastly some goals may not have expenses or savings, so they're just 0 + top_categories.append((goal_cat, 0)) + + #re-sort + top_categories.sort(key=lambda x: x[1], reverse=True) + + + budget_categories = [] for category, avg_spent in top_categories: cat_type = category_types.get(category, 'want') monthly_recommended = self.calculate_category_budget( category, avg_spent, cat_type, - monthly_avg, category_types, pools + monthly_avg, category_types, pools, goals ) + scaled_recommended = self.scale_budget(monthly_recommended, period) diff --git a/app/server/services/python/analytics/src/logic/savings.py b/app/server/services/python/analytics/src/logic/savings.py index b70a5a4..cb655bd 100644 --- a/app/server/services/python/analytics/src/logic/savings.py +++ b/app/server/services/python/analytics/src/logic/savings.py @@ -35,7 +35,7 @@ def calculate_savings_over_time( else: df['year_month'] = df['date'].dt.to_period('M') monthly_groups = df.groupby('year_month') - for month in pd.date_range(start=start_date, end=end_date, freq='M'): + for month in pd.date_range(start=start_date, end=end_date, freq='M'):#change this for production - GitHub actions might fail if this is ME, and work with M instead month_str = month.strftime('%Y-%m') month_period = pd.Period(month_str, freq='M') if month_period in monthly_groups.groups: diff --git a/app/server/services/python/analytics/src/queries/budget.py b/app/server/services/python/analytics/src/queries/budget.py index 7d1d9a4..6306a78 100644 --- a/app/server/services/python/analytics/src/queries/budget.py +++ b/app/server/services/python/analytics/src/queries/budget.py @@ -24,4 +24,39 @@ def get_user_transactions_with_connection(user_id: int, start_date: str, end_dat return get_user_transactions(cursor, user_id, start_date, end_date) finally: cursor.close() - connection.close() \ No newline at end of file + connection.close() + +#this was added to make goals influence the budget - analytics service will fetch goals on its own +def get_user_goals(user_id: int) -> List[Dict]: + """Fetch active goals for a user.""" + connection = get_db_connection() + cursor = connection.cursor(dictionary=True) + + #first get the user's profile + cursor.execute(""" + SELECT profile_id + FROM finus.finusAccount_profile + WHERE account_id = %s + """, (user_id,)) + profile = cursor.fetchone() + + if not profile: + cursor.close() + connection.close() + return [] + + profile_id = profile['profile_id'] + + #fetch goals for this profile + cursor.execute(""" + SELECT g.* + FROM finus.goal g + JOIN finus.profile_goal pg ON g.id = pg.goal_id + WHERE pg.profile_id = %s + """, (profile_id,)) + + goals = cursor.fetchall() + cursor.close() + connection.close() + + return goals \ No newline at end of file diff --git a/app/server/services/python/analytics/test/conftest.py b/app/server/services/python/analytics/test/conftest.py index 67187ae..5374bc6 100644 --- a/app/server/services/python/analytics/test/conftest.py +++ b/app/server/services/python/analytics/test/conftest.py @@ -62,23 +62,6 @@ def mock_db_queries(): yield mock -@pytest.fixture -def mock_savings_accounts(): - return [ - {'id': 1, 'balance': 5000}, - {'id': 2, 'balance': 3000}, - ] - -@pytest.fixture -def mock_savings_transactions(): - base_date = datetime.now() - timedelta(days=30) - return [ - {'financialAccount_id': 1, 'amount': 100, 'date': base_date + timedelta(days=5)}, - {'financialAccount_id': 1, 'amount': 200, 'date': base_date + timedelta(days=10)}, - {'financialAccount_id': 2, 'amount': -50, 'date': base_date + timedelta(days=7)}, - {'financialAccount_id': 2, 'amount': 150, 'date': base_date + timedelta(days=15)}, - {'financialAccount_id': 1, 'amount': -75, 'date': base_date + timedelta(days=20)}, - ] @pytest.fixture def mock_incomeflow_transactions(): diff --git a/app/server/services/python/analytics/test/test_budgetCalc.py b/app/server/services/python/analytics/test/test_budgetCalc.py index 87d6565..7811609 100644 --- a/app/server/services/python/analytics/test/test_budgetCalc.py +++ b/app/server/services/python/analytics/test/test_budgetCalc.py @@ -103,4 +103,520 @@ async def test_generate_budget(self, calculator, sample_transactions): assert hasattr(result, 'budget') assert hasattr(result, 'generated_date') - assert len(result.budget) <= 10 # At most 10 categories \ No newline at end of file + assert len(result.budget) <= 10 # At most 10 categories + + + #check if multiple saving goals adds 5% boost each + @pytest.mark.asyncio + async def test_calculate_goal_impact_save_goals(self, calculator): + goals = [ + {'type': 'save', 'category': 'vacation'}, + {'type': 'save', 'category': 'emergency_fund'}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {} + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + # 2 goals * 5% of $5000 = $500 + assert savings_boost == 500 + assert wants_reduction == 0 + assert set(categories) == {'vacation', 'emergency_fund'} + + + #check if a single savings goal adds a 5% boost + @pytest.mark.asyncio + async def test_calculate_goal_impact_single_save_goal(self, calculator): + goals = [ + {'type': 'save', 'category': 'vacation'}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {} + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 250 # 5% of $5000 + assert wants_reduction == 0 + assert categories == ['vacation'] + + + + #test reduce spending goal when spending exceeds limit (>=100%) + @pytest.mark.asyncio + async def test_calculate_goal_impact_reduce_spending_over_limit(self, calculator): + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 400}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {'groceries': 450} # Over limit + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 0 + assert wants_reduction == 5000 * 0.07 # 7% of $5000 = $350 + assert categories == ['groceries'] + + + #test reduce spending goal when spending is 80-99% of limit + @pytest.mark.asyncio + async def test_calculate_goal_impact_reduce_spending_approaching_limit(self, calculator): + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 100}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {'groceries': 87.5} # 87.5% of limit + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 0 + assert wants_reduction == 5000 * 0.05 # 5% of $5000 = $250 + assert categories == ['groceries'] + + + #test reduce spending goal when spending is 60-79% of limit + @pytest.mark.asyncio + async def test_calculate_goal_impact_reduce_spending_getting_close(self, calculator): + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 100}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {'groceries': 70} # 70% of limit + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 0 + assert wants_reduction == 5000 * 0.03 # 3% of $5000 = $150 + assert categories == ['groceries'] + + + #test reduce spending goal when spending is below 60% of limit + @pytest.mark.asyncio + async def test_calculate_goal_impact_reduce_spending_below_threshold(self, calculator): + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 100}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {'groceries': 50} # 50% of limit + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 0 + assert wants_reduction == 0 + assert categories == ['groceries'] + + + #test multiple reduce spending goals - should take the max reduction + @pytest.mark.asyncio + async def test_calculate_goal_impact_multiple_reduce_spending_goals(self, calculator): + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 100}, + {'type': 'reduce_spending', 'category': 'dining', 'target': 100}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = { + 'groceries': 101, #over limit - 7% reduction + 'dining': 83, # 83% of limit - 5% reduction + } + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 0 + #should take the max reduction (7% from groceries) + assert wants_reduction == 5000 * 0.07 # $350 + assert set(categories) == {'groceries', 'dining'} + + + #test mixed save and reduce spending goals + @pytest.mark.asyncio + async def test_calculate_goal_impact_mixed_goals(self, calculator): + goals = [ + {'type': 'save', 'category': 'vacation'}, + {'type': 'reduce_spending', 'category': 'groceries', 'target': 100}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {'groceries': 101} # Over limit + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + #save goal: 5% boost = $250 + assert savings_boost == 250 + #reduce spending: 7% reduction = $350 + assert round(wants_reduction) == 350 + assert set(categories) == {'vacation', 'groceries'} + + + #test category budget calculation with a reduce_spending goal cap + @pytest.mark.asyncio + async def test_calculate_category_budget_with_reduce_spending_goal(self, calculator): + monthly_avg = {'groceries': 300} + category_types = {'groceries': 'want'} + pools = {'needs': 2000, 'wants': 1000, 'savings': 500} + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 250} + ] + + #without goal, recommended would be (300 / 300) * 1000 = 1000 + #with goal, should be capped at 250 + result = calculator.calculate_category_budget( + 'groceries', 300, 'want', monthly_avg, category_types, pools, goals + ) + + assert result == 250 + + + #test category budget where goal cap is higher than calculated budget + @pytest.mark.asyncio + async def test_calculate_category_budget_with_goal_above_cap(self, calculator): + monthly_avg = {'groceries': 300} + category_types = {'groceries': 'want'} + pools = {'needs': 2000, 'wants': 500, 'savings': 500} + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 400} + ] + + #without goal: (300 / 300) * 500 = 500 + #goal cap is 400, but calculated is 500, so cap to 400 + result = calculator.calculate_category_budget( + 'groceries', 300, 'want', monthly_avg, category_types, pools, goals + ) + + assert result == 400 + + + #test category budget where goal doesn't affect the calculation + @pytest.mark.asyncio + async def test_calculate_category_budget_with_no_goal_impact(self, calculator): + monthly_avg = {'groceries': 300} + category_types = {'groceries': 'want'} + pools = {'needs': 2000, 'wants': 1000, 'savings': 500} + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 500} + ] + + # Calculated budget is 1000, goal cap is 500, so cap to 500 + result = calculator.calculate_category_budget( + 'groceries', 300, 'want', monthly_avg, category_types, pools, goals + ) + + # Since calculated budget is 1000, it gets capped to 500 + assert result == 500 + + + + #test full budget generation with goals that influence the pools + @pytest.mark.asyncio + async def test_generate_budget_with_goals(self, calculator, sample_transactions): + avg_income = 5000 + goals = [ + {'type': 'save', 'category': 'vacation'}, + {'type': 'reduce_spending', 'category': 'groceries', 'target': 300}, + ] + + #create transactions with high grocery spending to trigger reduction + sample_transactions.append({'amount': -350, 'category': 'groceries', 'date': '2024-01-15'}) + + result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals) + + assert hasattr(result, 'budget') + assert len(result.budget) > 0 + + #verify that the grocery category appears and has a capped budget + grocery_goal = next((g for g in result.budget if g.category == 'groceries'), None) + if grocery_goal: + #should be capped at 300 (or less) + assert grocery_goal.recommended_budget <= 300 + + + #test full budget generation with only save goals + @pytest.mark.asyncio + async def test_generate_budget_with_save_goal_only(self, calculator, sample_transactions): + avg_income = 5000 + + goals = [ + {'type': 'save', 'category': 'vacation'}, + {'type': 'save', 'category': 'emergency_fund'}, + ] + + result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals) + + assert hasattr(result, 'budget') + #verify goal categories are included + goal_categories = {g.category for g in result.budget} + assert 'vacation' in goal_categories + assert 'emergency_fund' in goal_categories + + + #test full budget generation with reduce spending goals + @pytest.mark.asyncio + async def test_generate_budget_with_reduce_spending_goal(self, calculator, sample_transactions): + avg_income = 5000 + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 300}, + ] + + #add high grocery spending + sample_transactions.append({'amount': -350, 'category': 'groceries', 'date': '2024-01-15'}) + + result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals) + + assert hasattr(result, 'budget') + + #verify grocery budget is capped + grocery = next((g for g in result.budget if g.category == 'groceries'), None) + if grocery: + assert grocery.recommended_budget <= 300 + + + #test that goal categories appear even if not in top 10 + @pytest.mark.asyncio + async def test_generate_budget_with_goal_category_not_in_top_10(self, calculator, sample_transactions): + avg_income = 5000 + + #add a goal for a category with very low spending + goals = [ + {'type': 'save', 'category': 'rare_category'}, + ] + + #add a small transaction for this category + sample_transactions.append({'amount': -10, 'category': 'rare_category', 'date': '2024-01-15'}) + + result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals) + + #verify the goal category appears in the budget + goal_categories = {g.category for g in result.budget} + assert 'rare_category' in goal_categories + + + #test full budget generation with no goals (should work same as before) + @pytest.mark.asyncio + async def test_generate_budget_with_no_goals(self, calculator, sample_transactions): + avg_income = 5000 + + result = calculator.generate_budget(sample_transactions, 'w', avg_income, None) + + assert hasattr(result, 'budget') + assert len(result.budget) > 0 + + + #test full budget generation with empty goals list + @pytest.mark.asyncio + async def test_generate_budget_with_empty_goals_list(self, calculator, sample_transactions): + avg_income = 5000 + + result = calculator.generate_budget(sample_transactions, 'w', avg_income, []) + + assert hasattr(result, 'budget') + assert len(result.budget) > 0 + + + #test reduce spending goal with zero target (should be ignored). + @pytest.mark.asyncio + async def test_calculate_goal_impact_zero_target(self, calculator): + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 0}, + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {'groceries': 450} + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 0 + assert wants_reduction == 0 + assert categories == ['groceries'] + + + #test goal with missing category (should still work) + @pytest.mark.asyncio + async def test_calculate_goal_impact_missing_category(self, calculator): + goals = [ + {'type': 'save'}, #no category + {'type': 'reduce_spending', 'target': 400}, #no category + ] + avg_monthly_income = 5000 + monthly_avg_by_category = {} + + savings_boost, wants_reduction, categories = calculator.calculate_goal_impact( + goals, avg_monthly_income, monthly_avg_by_category + ) + + assert savings_boost == 250 #save goal still applies + assert wants_reduction == 0 + assert categories == [] #no categories added since they're missing + + + #test category budget when pool_actual is zero (no expenses in any want category) + @pytest.mark.asyncio + async def test_calculate_category_budget_with_zero_pool_actual(self, calculator): + monthly_avg = {'groceries': 300} + category_types = {'groceries': 'want'} + + monthly_avg = {} #no want categories have any spending + category_types = {'new_category': 'want'} + pools = {'needs': 2000, 'wants': 1000, 'savings': 500} + goals = [] + + result = calculator.calculate_category_budget( + 'new_category', 0, 'want', monthly_avg, category_types, pools, goals + ) + + #when pool_actual is 0 and avg_spent is 0, recommended = 0 * 0.9 = 0 + assert result == 0 + + #test that wants_reduction properly reduces wants and increases savings + @pytest.mark.asyncio + async def test_generate_budget_with_wants_reduction(self, calculator): + avg_income = 5000 + + groceries_per_month = 450 + grocery_transactions = [] + for month in range(1, 13): + grocery_transactions.append({ + 'amount': -groceries_per_month, + 'category': 'groceries', + 'date': f'2024-{month:02d}-15' + }) + + base_transactions = [ + {'amount': 5000, 'category': 'salary', 'date': '2024-01-01'}, + {'amount': -200, 'category': 'dining', 'date': '2024-01-02'}, + {'amount': -150, 'category': 'dining', 'date': '2024-01-03'}, + {'amount': -100, 'category': 'entertainment', 'date': '2024-01-04'}, + ] + + # Add monthly salary transactions + salary_transactions = [] + for month in range(1, 13): + salary_transactions.append({ + 'amount': 5000, + 'category': 'salary', + 'date': f'2024-{month:02d}-01' + }) + + all_transactions = salary_transactions + grocery_transactions + base_transactions + + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 400}, + ] + + avg_income = 5000 + + result = calculator.generate_budget(all_transactions, 'w', avg_income, goals) + + assert hasattr(result, 'budget') + assert len(result.budget) > 0 + wants_budget_total = 0 + for cat in result.budget: + if cat.type == 'want': + wants_budget_total += cat.recommended_budget + assert wants_budget_total <= 1200, f"Wants budget {wants_budget_total} should be reduced from baseline" + grocery = next((cat for cat in result.budget if cat.category == 'groceries'), None) + if grocery: + assert grocery.recommended_budget <= 400 # Should be capped at target + + + + #test with multiple reduce spending goals to ensure max reduction is applied + @pytest.mark.asyncio + async def test_generate_budget_with_multiple_reduce_spending_goals(self, calculator): + transactions = [] + for month in range(1, 13): + transactions.append({ + 'amount': 5000, 'category': 'salary', 'date': f'2024-{month:02d}-01' + }) + for month in range(1, 13): + transactions.append({ + 'amount': -450, 'category': 'groceries', 'date': f'2024-{month:02d}-10' + }) + + for month in range(1, 13): + transactions.append({ + 'amount': -350, 'category': 'dining', 'date': f'2024-{month:02d}-15' + }) + for month in range(1, 13): + transactions.append({ + 'amount': -250, 'category': 'entertainment', 'date': f'2024-{month:02d}-20' + }) + + avg_income = 5000 + + goals = [ + {'type': 'reduce_spending', 'category': 'groceries', 'target': 400}, #over limit: 450 -> 7% reduction + {'type': 'reduce_spending', 'category': 'dining', 'target': 400}, #350/400=87.5% -> 5% reduction + {'type': 'reduce_spending', 'category': 'entertainment', 'target': 300}, #250/300=83.3% -> 5% reduction + ] + + result = calculator.generate_budget(transactions, 'w', avg_income, goals) + + wants_budget_total = sum( + cat.recommended_budget for cat in result.budget if cat.type == 'want' + ) + + assert wants_budget_total <= 1200 + grocery = next((cat for cat in result.budget if cat.category == 'groceries'), None) + dining = next((cat for cat in result.budget if cat.category == 'dining'), None) + + if grocery: + assert grocery.recommended_budget <= 400 + if dining: + assert dining.recommended_budget <= 400 + + + #test multiple goal categories covering all three branches + @pytest.mark.asyncio + async def test_generate_budget_multiple_goal_categories_all_branches(self, calculator): + transactions = [] + for month in range(1, 13): + transactions.append({'amount': 5000, 'category': 'salary', 'date': f'2024-{month:02d}-01'}) + + for month in range(1, 13): + transactions.append({'amount': -5000, 'category': 'rent', 'date': f'2024-{month:02d}-05'}) + transactions.append({'amount': -1000, 'category': 'groceries', 'date': f'2024-{month:02d}-10'}) + transactions.append({'amount': -800, 'category': 'dining', 'date': f'2024-{month:02d}-12'}) + transactions.append({'amount': -600, 'category': 'entertainment', 'date': f'2024-{month:02d}-15'}) + + for month in range(1, 13): + transactions.append({'amount': -50, 'category': 'groceries_small', 'date': f'2024-{month:02d}-20'}) + + for month in range(1, 13): + transactions.append({'amount': 150, 'category': 'investment', 'date': f'2024-{month:02d}-25'}) + + avg_income = 5000 + 150 + + goals = [ + {'type': 'save', 'category': 'groceries_small'}, #in monthly_avg + {'type': 'save', 'category': 'investment'}, #in monthly_avg_savings + {'type': 'save', 'category': 'new_goal'}, #in neither + ] + + result = calculator.generate_budget(transactions, 'w', avg_income, goals) + + goal_categories = {cat.category for cat in result.budget} + assert 'groceries_small' in goal_categories + assert 'investment' in goal_categories + assert 'new_goal' in goal_categories + + groceries = next(cat for cat in result.budget if cat.category == 'groceries_small') + investment = next(cat for cat in result.budget if cat.category == 'investment') + new_goal = next(cat for cat in result.budget if cat.category == 'new_goal') + + assert groceries.avg_monthly_spent > 0 + assert investment.avg_monthly_spent > 0 + assert new_goal.avg_monthly_spent == 0 \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_budgetPerfCalc.py b/app/server/services/python/analytics/test/test_budgetPerfCalc.py index dfa3e17..12cd384 100644 --- a/app/server/services/python/analytics/test/test_budgetPerfCalc.py +++ b/app/server/services/python/analytics/test/test_budgetPerfCalc.py @@ -13,6 +13,49 @@ class TestBudgetPerformanceCalculator: def performance_calc(self): return BudgetPerformanceCalculator() + @pytest.fixture + def sample_budget_response_with_high_targets(self): + categories = [ + BudgetCategory( + category='groceries', + type='need', + avg_monthly_spent=300, + monthly_budget=280, + recommended_budget=500, + is_essential=True + ), + BudgetCategory( + category='dining', + type='want', + avg_monthly_spent=200, + monthly_budget=150, + recommended_budget=400, + is_essential=False + ), + BudgetCategory( + category='entertainment', + type='want', + avg_monthly_spent=150, + monthly_budget=120, + recommended_budget=300, + is_essential=False + ), + ] + return BudgetResponse( + budget=categories, + generated_date='2024-03-15 12:00:00' + ) + + @pytest.fixture + def mock_transactions_low_spending(self): + """Create transactions with low actual spending.""" + return [ + {'category': 'groceries', 'amount': -50, 'date': '2024-03-10'}, + {'category': 'groceries', 'amount': -30, 'date': '2024-03-12'}, + {'category': 'dining', 'amount': -40, 'date': '2024-03-14'}, + {'category': 'entertainment', 'amount': -20, 'date': '2024-03-15'}, + ] + @pytest.fixture def mock_transactions(self): return [ @@ -131,6 +174,32 @@ async def test_generate_performance_empty_transactions( # All actual amounts should be 0 assert all(amount == 0 for amount in result['actualAmounts']) mock_get.assert_called_once() + + + async def test_calculate_performance_underflow(self, performance_calc, sample_budget_response_with_high_targets): + #create actual spending that is much lower than budget - this will cause variance < -100 - underflow + actual_spending = { + 'groceries': 50, + 'dining': 30, + 'entertainment': 20 + } + + result = performance_calc.calculate_performance( + sample_budget_response_with_high_targets.budget, + actual_spending, + 'w' + ) + + assert len(result) > 0 + + #verify all variances are less than -100 + for i, category in enumerate(result['categories']): + budget = sample_budget_response_with_high_targets.budget[i].recommended_budget + actual = actual_spending[category] + variance = actual - budget + + assert variance < -100 + async def test_generate_performance_db_error( self, diff --git a/app/server/services/python/analytics/test/test_budget_integration.py b/app/server/services/python/analytics/test/test_budget_integration.py index 56724c3..df131ad 100644 --- a/app/server/services/python/analytics/test/test_budget_integration.py +++ b/app/server/services/python/analytics/test/test_budget_integration.py @@ -18,11 +18,15 @@ async def test_full_budget_flow(self): {'amount': 5000, 'category': 'salary', 'date': '2024-02-01'}, {'amount': -250, 'category': 'groceries', 'date': '2024-02-02'}, ] + + mock_goals = [] - with patch( - 'src.logic.budget.get_user_transactions_with_connection', - return_value=mock_transactions - ): + with patch('src.logic.budget.get_user_transactions_with_connection') as mock_transactions_query, \ + patch('src.logic.budget.get_user_goals') as mock_goals_query: + + mock_transactions_query.return_value = mock_transactions + mock_goals_query.return_value = mock_goals + # Generate budget budget = generate_budget(period, user_id) diff --git a/app/server/services/python/analytics/test/test_dependencies.py b/app/server/services/python/analytics/test/test_dependencies.py index df09d1d..b148cde 100644 --- a/app/server/services/python/analytics/test/test_dependencies.py +++ b/app/server/services/python/analytics/test/test_dependencies.py @@ -1,3 +1,5 @@ +import importlib + import pytest from unittest.mock import patch, MagicMock import mysql.connector @@ -5,7 +7,6 @@ from fastapi import HTTPException import os import jwt -from jwt import InvalidSignatureError class TestDatabaseConnection: @@ -33,7 +34,7 @@ def test_get_db_connection_missing_env_vars(self, mock_connect): @patch('mysql.connector.connect') def test_get_db_connection_failure(self, mock_connect): - mock_connect.side_effect = mysql.connector.Error("Connection failed") + mock_connect.side_effect = mysql.connector.Error('Connection failed') with patch.dict(os.environ, {'MYSQL_HOST': 'localhost', 'MYSQL_USER': 'test_user', 'MYSQL_PASSWORD': 'test_password', 'DB_NAME': 'test_db'}, clear=True): with pytest.raises(mysql.connector.Error): get_db_connection() @@ -41,11 +42,29 @@ def test_get_db_connection_failure(self, mock_connect): class TestJWTAuthentication: + + @pytest.mark.asyncio + async def test_get_current_user_perfect_case(self): + payload = {'sub': '123'} + token = jwt.encode(payload, 'test', algorithm='HS256') + + if isinstance(token, bytes): + token = token.decode('utf-8') + + with patch.dict(os.environ, {'JWT_SECRET': 'test'}): + import src.dependencies + importlib.reload(src.dependencies) + from src.dependencies import get_current_user, SECRET_KEY + + assert SECRET_KEY == 'test' + + result = await get_current_user(authorization=f'Bearer {token}') + assert result == 123 @pytest.mark.asyncio async def test_get_current_user_missing_user_id(self): - payload = {'email': 'test@example.com'} + payload = {'sub': None} token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') # Ensure token is string @@ -54,11 +73,11 @@ async def test_get_current_user_missing_user_id(self): with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): with pytest.raises(HTTPException) as exc_info: - await get_current_user(authorization=f"Bearer {token}") + await get_current_user(authorization=f'Bearer {token}') - assert exc_info.value.status_code == 401 - assert exc_info.value.detail == "User ID not found in token" - + assert exc_info.value.status_code == 401 + + @pytest.mark.asyncio async def test_get_current_user_invalid_signature(self): @@ -70,7 +89,38 @@ async def test_get_current_user_invalid_signature(self): with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): with pytest.raises(HTTPException) as exc_info: - await get_current_user(authorization=f"Bearer {token}") + await get_current_user(authorization=f'Bearer {token}') + + assert exc_info.value.status_code == 401 + + @pytest.mark.asyncio + async def test_get_current_user_no_auth_header(self): + + payload = {'sub': '123'} + token = jwt.encode(payload, 'wrong_secret', algorithm='HS256') + + if isinstance(token, bytes): + token = token.decode('utf-8') + + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): + with pytest.raises(HTTPException) as exc_info: + await get_current_user(authorization=None) + + assert exc_info.value.status_code == 401 + assert exc_info.value.detail == 'Authorization header missing' + + @pytest.mark.asyncio + async def test_get_current_user_no_bearer(self): + + payload = {'sub': '123'} + token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') + + if isinstance(token, bytes): + token = token.decode('utf-8') + + with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): + with pytest.raises(HTTPException) as exc_info: + await get_current_user(authorization=f'something {token}') assert exc_info.value.status_code == 401 - assert "token" in exc_info.value.detail.lower() \ No newline at end of file + # assert exc_info.value.detail == '401: Invalid authentication scheme' \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_savings.py b/app/server/services/python/analytics/test/test_savings.py index 02d7216..a1f94de 100644 --- a/app/server/services/python/analytics/test/test_savings.py +++ b/app/server/services/python/analytics/test/test_savings.py @@ -3,8 +3,32 @@ from datetime import datetime from unittest.mock import patch, MagicMock from src.logic.savings import calculate_savings_over_time +from datetime import datetime, timedelta class TestSavings: + + @pytest.fixture + def mock_savings_transactions(self): + base_date = pd.Timestamp('2024-03-15') + return [ + {'financialAccount_id': 1, 'amount': 100, 'date': base_date - timedelta(days=5)}, + {'financialAccount_id': 1, 'amount': 200, 'date': base_date - timedelta(days=10)}, + {'financialAccount_id': 2, 'amount': -50, 'date': base_date - timedelta(days=7)}, + {'financialAccount_id': 2, 'amount': 150, 'date': base_date - timedelta(days=15)}, + {'financialAccount_id': 1, 'amount': -75, 'date': base_date - timedelta(days=20)}, + {'financialAccount_id': 1, 'amount': -25, 'date': base_date - timedelta(days=5)}, + {'financialAccount_id': 1, 'amount': -25, 'date': base_date - timedelta(days=5)}, + {'financialAccount_id': 1, 'amount': 25, 'date': base_date}, + {'financialAccount_id': 1, 'amount': 25, 'date': base_date}, + {'financialAccount_id': 1, 'amount': 25, 'date': base_date - timedelta(days=62)}, + ] + + @pytest.fixture + def mock_savings_accounts(self): + return [ + {'id': 1, 'balance': 5000}, + {'id': 2, 'balance': 3000}, + ] def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_savings_transactions): end_date = pd.Timestamp('2024-03-15') @@ -21,10 +45,9 @@ def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_sa assert 'labels' in result assert 'datasets' in result - assert len(result['labels']) == 8 # 7 days + 1? Check your implementation + assert len(result['labels']) == 8 assert result['datasets'][0]['label'] == 'Weekly Savings' - - # Verify data is numeric + for value in result['datasets'][0]['data']: assert isinstance(value, (int, float)) diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 65cf47e..b286e95 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -2,7 +2,7 @@ import { PORT } from "@/port"; import { onExit } from "@/hooks"; import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; -import { buildCorsConfig } from "@/expressUtils"; +import { buildCorsConfig } from "@/expressUtils.ts"; import * as fs from "node:fs"; import * as path from "node:path"; import { randomUUID } from "node:crypto"; @@ -52,7 +52,14 @@ app.use( }), ); -const USER_PATHS = ["accounts", "profiles", "charts/expenses"]; +const USER_PATHS = [ + "accounts", + "profiles", + "/charts/expenses", + "/table/transactions", + "/table/snapshot", + "/goals", +]; app.use( createProxyMiddleware({ pathFilter: (path) => pathMatches(path, USER_PATHS), @@ -81,16 +88,6 @@ app.use( }), ); -// const ANALYTICS_PATHS = ["charts/savings", "charts/incomeflow"]; -// app.use( -// createProxyMiddleware({ -// pathFilter: (path) => pathMatches(path, ANALYTICS_PATHS), -// target: process.env.ANALYTICS_SERVICE_ADDR, -// changeOrigin: true, -// pathRewrite: { "^/api": "" }, -// }), -// ); - app.get("/health", async (req: express.Request, res: express.Response) => { const result: { [key: string]: string } = {}; const services: string[] = Object.keys(process.env).filter((x) => @@ -120,49 +117,32 @@ const server = app.listen(PORT, () => { }); onExit(async () => await server.close()); -//API gateway sits on port 3000 and is accessible from there. Go to browser and type http://localhost:3000/health and you should see which services are up. -// app.get('/health', async (req: express.Request, res: express.Response) => { -// const result: { [serviceName: string]: string } = {}; -// const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x)); - -// for(const service of services) { -// const serviceName = service.split('_')[0]; -// if (serviceName) -// result[serviceName] = await fetch(`${process.env[service]}/health`) -// .then((res) => res.text()) -// .catch((err) => err.message); -// console.log("received response"); -// } - -// res.json(result); -// }); - //expenses bar chart in user service -app.use( - createProxyMiddleware({ - pathFilter: ["/charts/expenses"], - target: process.env.USER_SERVICE_ADDR, - changeOrigin: true, - }), -); +// app.use( +// createProxyMiddleware({ +// pathFilter: ["/charts/expenses"], +// target: process.env.USER_SERVICE_ADDR, +// changeOrigin: true, +// }), +// ); //transactions table in user service -app.use( - createProxyMiddleware({ - pathFilter: ["/table/transactions"], - target: process.env.USER_SERVICE_ADDR, - changeOrigin: true, - }), -); +// app.use( +// createProxyMiddleware({ +// pathFilter: ["/table/transactions"], +// target: process.env.USER_SERVICE_ADDR, +// changeOrigin: true, +// }), +// ); //snapshot of total values like debt, savings, etc from user service -app.use( - createProxyMiddleware({ - pathFilter: ["/table/snapshot"], - target: process.env.USER_SERVICE_ADDR, - changeOrigin: true, - }), -); +// app.use( +// createProxyMiddleware({ +// pathFilter: ["/table/snapshot"], +// target: process.env.USER_SERVICE_ADDR, +// changeOrigin: true, +// }), +// ); //savings chart from analytics service app.use( diff --git a/app/server/services/ts/bun.lock b/app/server/services/ts/bun.lock index cdfef39..ad93995 100644 --- a/app/server/services/ts/bun.lock +++ b/app/server/services/ts/bun.lock @@ -5,16 +5,17 @@ "": { "dependencies": { "@types/cors": "^2.8.19", - "@vitest/coverage-v8": "^4.0.18", + "@vitest/coverage-v8": "^4.1.1", "cors": "^2.8.6", "express": "^5.2.1", "mysql2": "^3.18.1", + "stryker-mutator-bun-runner": "^0.4.0", }, "devDependencies": { "@eslint/css": "^0.14.1", "@eslint/js": "^10.0.1", "@eslint/json": "^1.0.0", - "eslint": "^9", + "eslint": "^10.1.0", "globals": "^17.3.0", "husky": "^9.1.7", "jiti": "^2.6.1", @@ -29,12 +30,68 @@ }, }, "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="], + + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], + + "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], @@ -95,9 +152,9 @@ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + "@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="], "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], @@ -105,13 +162,11 @@ "@eslint/css-tree": ["@eslint/css-tree@3.6.9", "", { "dependencies": { "mdn-data": "2.23.0", "source-map-js": "^1.0.1" } }, "sha512-3D5/OHibNEGk+wKwNwMbz63NMf367EoR4mVNNpxddCHKEb2Nez7z62J2U6YjtErSsZDoY0CsccmoUpdEbkogNA=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], - "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], "@eslint/json": ["@eslint/json@1.0.1", "", { "dependencies": { "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-bE2nGv8/U+uRvQEJWOgCsZCa65XsCBgxyyx/sXtTHVv0kqdauACLzyp7A1C3yNn7pRaWjIt5acxY+TAbSyIJXw=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], @@ -125,12 +180,74 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="], + + "@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="], + + "@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/editor": ["@inquirer/editor@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/external-editor": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA=="], + + "@inquirer/expand": ["@inquirer/expand@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@2.0.4", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.4", "", {}, "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ=="], + + "@inquirer/input": ["@inquirer/input@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ=="], + + "@inquirer/number": ["@inquirer/number@4.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA=="], + + "@inquirer/password": ["@inquirer/password@5.0.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.3.2", "", { "dependencies": { "@inquirer/checkbox": "^5.1.2", "@inquirer/confirm": "^6.0.10", "@inquirer/editor": "^5.0.10", "@inquirer/expand": "^5.0.10", "@inquirer/input": "^5.0.10", "@inquirer/number": "^4.0.10", "@inquirer/password": "^5.0.10", "@inquirer/rawlist": "^5.2.6", "@inquirer/search": "^4.1.6", "@inquirer/select": "^5.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.2.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w=="], + + "@inquirer/search": ["@inquirer/search@4.1.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ=="], + + "@inquirer/select": ["@inquirer/select@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA=="], + + "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/8IzqSu4/OWGRs7Fs2ROzGVwJMFTBQkgAp6sAthkBYoN7OiM4rY/CpPVs2X9w9N1W61CHSkEdNKi8HrLZKfK3g=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-TT7eUihnAzxM2tlZesusuC75PAOYKvUBgVU/Nm/lakZ/DpyuqhNkzUfcxSgmmK9IjVWzMmezLIGZl16XGCGJng=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-CYjIHWaQG7T4phfjErHr6BiXRs0K/9DqMeiohJmuYSBF+H2m56vFslOenLCguGYQL9jeiiCZBeoVCpwjxZrMgQ=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-8XMLyRNxHF4jfLajkWt+F8UDxsWbzysyxQVMZKUXwoeGvaxB0rVd07r3YbgDtG8U6khhRFM3oaGp+CQ0whwmdA=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jBwYCLG5Eb+PqtFrc3Wp2WMYlw1Id75gUcsdP+ApCOpf5oQhHxkFWCjZmcDoioDmEhMWAiM3wtwSrTlPg+sI6Q=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-z3GFCk1UBzDOOiEBHL32lVP7Edi26BhOjKb6bIc0nRyabbRiyON4++GR0zmd/H5zM5S0+UcXFgCGnD+b8avTLw=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-KZlf1jKtf4jai8xiQv/0XRjxVVhHnw/HtUKtLdOeQpTOQ1fQFhLoz2FGGtVRd0LVa/yiRbSz9HlWIzWlmJClng=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-ADImD4yCHNpqZu718E2chWcCaAHvua90yhmpzzV6fF4zOhwkGGbPCgUWmKyJ83uz+DXaPdYxX0ttDvtolrzx3Q=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-J+qz4Al05PrNIOdj7xsWVTyx0c/gjUauG5nKV3Rrx0Q+5JO+1pPVlnfNmWbOF9pKG4f3IGad8KXJUfGMORld+Q=="], + + "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UOdkwScHRkGPz+n9ZJU7sTkTvqV7rD1SLCLaru1xH8WRsV7tDorPqNCzEN1msOIiPRK825nvAtEm9UsomO1GsA=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-E51tyWDP1l0CbjZYhiUxhDGPaY8Hf5YBREx0PHBff1LM1/q3qsJ6ZvRUa8YbbOO0Ax9QP6GHjD9vf3n6bXZ7QA=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-cCsXK9AQ9Zf18QlVnbrFu2IKfr4sf2sfbErkF2jfCzyCO9Bnhl0KRx63zlN+Ni1xU7gcBLAssgcui5R400N2eA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], @@ -181,14 +298,28 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@stryker-mutator/api": ["@stryker-mutator/api@9.6.0", "", { "dependencies": { "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-kJEEwOVoWDXGEIXuM+9efT6LSJ7nyxnQQvjEoKg8GSZXbDUjfD0tqA0aBD06U1SzQLKCM7ffjgPffr154MHZKw=="], + + "@stryker-mutator/core": ["@stryker-mutator/core@9.6.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/instrumenter": "9.6.0", "@stryker-mutator/util": "9.6.0", "ajv": "~8.18.0", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.2.4", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.7.2", "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.2.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-oSbw01l6HXHt0iW9x5fQj7yHGGT8ZjCkXSkI7Bsu0juO7Q6vRMXk7XcvKpCBgRgzKXi1osg8+iIzj7acHuxepQ=="], + + "@stryker-mutator/instrumenter": ["@stryker-mutator/instrumenter@9.6.0", "", { "dependencies": { "@babel/core": "~7.29.0", "@babel/generator": "~7.29.0", "@babel/parser": "~7.29.0", "@babel/plugin-proposal-decorators": "~7.29.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.28.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "angular-html-parser": "~10.4.0", "semver": "~7.7.0", "tslib": "2.8.1", "weapon-regex": "~1.3.2" } }, "sha512-tWdRYfm9LF4Go7cNOos0xEIOEnN7ZOSj38rfXvGZS9IINlvYBrBCl2xcz/67v6l5A7xksMWWByZRIq2bgdnnUg=="], + + "@stryker-mutator/util": ["@stryker-mutator/util@9.6.0", "", {}, "sha512-gw7fJOFNHEj9inAEOodD9RrrMEMhZmWJ46Ww/kDJAXlSsBBmdwCzeomNLngmLTvgp14z7Tfq85DHYwvmNMdOxA=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -215,7 +346,7 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.1", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.1", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.1.1", "vitest": "4.1.1" }, "optionalPeers": ["@vitest/browser"] }, "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw=="], "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], @@ -239,44 +370,50 @@ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="], + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "bun": ["bun@1.3.11", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.11", "@oven/bun-darwin-x64": "1.3.11", "@oven/bun-darwin-x64-baseline": "1.3.11", "@oven/bun-linux-aarch64": "1.3.11", "@oven/bun-linux-aarch64-musl": "1.3.11", "@oven/bun-linux-x64": "1.3.11", "@oven/bun-linux-x64-baseline": "1.3.11", "@oven/bun-linux-x64-musl": "1.3.11", "@oven/bun-linux-x64-musl-baseline": "1.3.11", "@oven/bun-windows-aarch64": "1.3.11", "@oven/bun-windows-x64": "1.3.11", "@oven/bun-windows-x64-baseline": "1.3.11" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-AvXWYFO6j/ZQ7bhGm4X6eilq2JHsDVC90ZM32k2B7/srhC2gs3Sdki1QTbwrdRCo8o7eT+167vcB1yzOvPdbjA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -293,10 +430,18 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -309,17 +454,19 @@ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], + "eslint": ["eslint@10.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], @@ -333,6 +480,8 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -343,8 +492,18 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -355,6 +514,8 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -365,10 +526,16 @@ "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="], @@ -385,14 +552,14 @@ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -403,10 +570,16 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], @@ -415,28 +588,38 @@ "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -457,10 +640,24 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + "minimatch": ["minimatch@5.1.8", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mutation-server-protocol": ["mutation-server-protocol@0.4.1", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g=="], + + "mutation-testing-elements": ["mutation-testing-elements@3.7.2", "", {}, "sha512-i7X2Q4X5eYon72W2QQ9HND7plVhQcqTnv+Xc3KeYslRZSJ4WYJoal8LFdbWm7dKWLNE0rYkCUrvboasWzF3MMA=="], + + "mutation-testing-metrics": ["mutation-testing-metrics@3.7.2", "", { "dependencies": { "mutation-testing-report-schema": "3.7.2" } }, "sha512-ichXZSC4FeJbcVHYOWzWUhNuTJGogc0WiQol8lqEBrBSp+ADl3fmcZMqrx0ogInEUiImn+A8JyTk6uh9vd25TQ=="], + + "mutation-testing-report-schema": ["mutation-testing-report-schema@3.7.2", "", {}, "sha512-fN5M61SDzIOeJyatMOhGPLDOFz5BQIjTNPjo4PcHIEUWrejO4i4B5PFuQ/2l43709hEsTxeiXX00H73WERKcDw=="], + + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "mysql2": ["mysql2@3.19.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-760Ay9HViAUT7V8QkYxrIx/LHJPGWtE+D/31VuiDm1eu2IFaUIqMcK7+hVHnmgnC7JnnwreFKsZoa8K6BnGl8Q=="], "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], @@ -471,6 +668,10 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -487,7 +688,9 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -495,6 +698,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -509,6 +714,10 @@ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -519,12 +728,14 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -549,6 +760,10 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], @@ -559,7 +774,9 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "stryker-mutator-bun-runner": ["stryker-mutator-bun-runner@0.4.0", "", { "dependencies": { "@stryker-mutator/api": "^9.0.1", "@stryker-mutator/util": "^9.0.1", "execa": "^9.6.0", "glob": "^11.0.3", "semver": "^7.5.4" }, "peerDependencies": { "@stryker-mutator/core": "^9.0.0", "bun": ">=1.0.0" } }, "sha512-wx2nfdfKFWRxE5fiAYzRBVJoUMtoeYIE2uAtWS1fu4rGeay6XVm3wBmqi2bKosVH6zHPShsciSCDKDGifZHvSA=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -573,20 +790,36 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="], + + "typed-rest-client": ["typed-rest-client@2.2.0", "", { "dependencies": { "des.js": "^1.1.0", "js-md4": "^0.3.2", "qs": "^6.14.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-/e2Rk9g20N0r44kaQLb3v6QGuryOD8SPb53t43Y5kqXXA+SqWuU7zLiMxetw61jNn/JFrxTdr5nPDhGY/eTNhQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -595,6 +828,8 @@ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -603,38 +838,78 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "@eslint/config-array/minimatch": ["minimatch@3.1.4", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@eslint/eslintrc/minimatch": ["minimatch@3.1.4", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/config-array/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@eslint/config-helpers/@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="], "@eslint/json/@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="], "@eslint/json/@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="], + "@stryker-mutator/core/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "@stryker-mutator/core/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.3", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="], + + "@vitest/coverage-v8/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "eslint/@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="], - "eslint/@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], + "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], - "eslint/minimatch": ["minimatch@3.1.4", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw=="], + "eslint/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + + "@stryker-mutator/core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@stryker-mutator/core/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], - "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="], + + "eslint/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + + "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "@stryker-mutator/core/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/app/server/services/ts/common/expressUtils.ts b/app/server/services/ts/common/expressUtils.ts index 30d7c2d..70a6c31 100644 --- a/app/server/services/ts/common/expressUtils.ts +++ b/app/server/services/ts/common/expressUtils.ts @@ -1,6 +1,6 @@ import cors from "cors"; -const defaultMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]; +const defaultMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]; const defaultOrigins = ["http://localhost", "http://localhost:8080"]; export function buildCorsConfig(opts?: { origins?: string | string[]; diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json index 5e3c1bf..e8d135a 100644 --- a/app/server/services/ts/package.json +++ b/app/server/services/ts/package.json @@ -7,7 +7,7 @@ "@eslint/css": "^0.14.1", "@eslint/js": "^10.0.1", "@eslint/json": "^1.0.0", - "eslint": "^9", + "eslint": "^10.1.0", "globals": "^17.3.0", "husky": "^9.1.7", "jiti": "^2.6.1", @@ -22,9 +22,10 @@ "type": "module", "dependencies": { "@types/cors": "^2.8.19", - "@vitest/coverage-v8": "^4.0.18", + "@vitest/coverage-v8": "^4.1.1", "cors": "^2.8.6", "express": "^5.2.1", - "mysql2": "^3.18.1" + "mysql2": "^3.18.1", + "stryker-mutator-bun-runner": "^0.4.0" } } diff --git a/app/server/services/ts/user/Dockerfile.integration.test b/app/server/services/ts/user/Dockerfile.integration.test index 72a01b0..51af2d2 100644 --- a/app/server/services/ts/user/Dockerfile.integration.test +++ b/app/server/services/ts/user/Dockerfile.integration.test @@ -6,4 +6,5 @@ WORKDIR /project COPY vitest.config.ts . ENV CI=true -CMD ["bun", "run", "vitest", "--run", "integration"] + +CMD ["bun", "test", "integration", "--coverage"] diff --git a/app/server/services/ts/user/Dockerfile.mutation.test b/app/server/services/ts/user/Dockerfile.mutation.test new file mode 100644 index 0000000..ec890cd --- /dev/null +++ b/app/server/services/ts/user/Dockerfile.mutation.test @@ -0,0 +1,10 @@ +FROM measureonecodetwice/finus-user:local + +COPY user/test ./test + +WORKDIR /project +COPY vitest.config.ts . + +ENV CI=true + +CMD [ "bun", "stryker", "run"] diff --git a/app/server/services/ts/user/bun.lock b/app/server/services/ts/user/bun.lock index ef41a73..21f8076 100644 --- a/app/server/services/ts/user/bun.lock +++ b/app/server/services/ts/user/bun.lock @@ -10,7 +10,9 @@ "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mysql2": "^3.15.3", - "vitest": "^4.1.1", + "stryker-mutator-bun-runner": "^0.4.0", + "supertest": "^7.2.2", + "vitest": "^4.0.18", }, "devDependencies": { "@eslint/js": "^9.39.2", @@ -19,7 +21,7 @@ "@types/node": "^25.1.0", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", - "eslint": "^9.39.2", + "eslint": "^10.1.0", "eslint-plugin-react": "^7.37.5", "globals": "^17.3.0", "jiti": "^2.6.1", @@ -32,6 +34,70 @@ }, }, "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="], + + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], + + "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -88,19 +154,17 @@ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + "@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="], - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], + "@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="], "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -110,12 +174,78 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="], + + "@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="], + + "@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/editor": ["@inquirer/editor@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/external-editor": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA=="], + + "@inquirer/expand": ["@inquirer/expand@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@2.0.4", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.4", "", {}, "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ=="], + + "@inquirer/input": ["@inquirer/input@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ=="], + + "@inquirer/number": ["@inquirer/number@4.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA=="], + + "@inquirer/password": ["@inquirer/password@5.0.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.3.2", "", { "dependencies": { "@inquirer/checkbox": "^5.1.2", "@inquirer/confirm": "^6.0.10", "@inquirer/editor": "^5.0.10", "@inquirer/expand": "^5.0.10", "@inquirer/input": "^5.0.10", "@inquirer/number": "^4.0.10", "@inquirer/password": "^5.0.10", "@inquirer/rawlist": "^5.2.6", "@inquirer/search": "^4.1.6", "@inquirer/select": "^5.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.2.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w=="], + + "@inquirer/search": ["@inquirer/search@4.1.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ=="], + + "@inquirer/select": ["@inquirer/select@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA=="], + + "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@mysql/xdevapi": ["@mysql/xdevapi@8.0.35", "", { "dependencies": { "google-protobuf": "3.19.4", "lossless-json": "2.0.1", "parsimmon": "1.18.1" } }, "sha512-l7HoBt1l0GwdCBXBrqri4kPBbJrqsyvaa4eqXvq50aWWArOTc5XGV06FWF1epxT+RORBy7scZTWFANxpr4Sfuw=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/8IzqSu4/OWGRs7Fs2ROzGVwJMFTBQkgAp6sAthkBYoN7OiM4rY/CpPVs2X9w9N1W61CHSkEdNKi8HrLZKfK3g=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-TT7eUihnAzxM2tlZesusuC75PAOYKvUBgVU/Nm/lakZ/DpyuqhNkzUfcxSgmmK9IjVWzMmezLIGZl16XGCGJng=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-CYjIHWaQG7T4phfjErHr6BiXRs0K/9DqMeiohJmuYSBF+H2m56vFslOenLCguGYQL9jeiiCZBeoVCpwjxZrMgQ=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-8XMLyRNxHF4jfLajkWt+F8UDxsWbzysyxQVMZKUXwoeGvaxB0rVd07r3YbgDtG8U6khhRFM3oaGp+CQ0whwmdA=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jBwYCLG5Eb+PqtFrc3Wp2WMYlw1Id75gUcsdP+ApCOpf5oQhHxkFWCjZmcDoioDmEhMWAiM3wtwSrTlPg+sI6Q=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-z3GFCk1UBzDOOiEBHL32lVP7Edi26BhOjKb6bIc0nRyabbRiyON4++GR0zmd/H5zM5S0+UcXFgCGnD+b8avTLw=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-KZlf1jKtf4jai8xiQv/0XRjxVVhHnw/HtUKtLdOeQpTOQ1fQFhLoz2FGGtVRd0LVa/yiRbSz9HlWIzWlmJClng=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-ADImD4yCHNpqZu718E2chWcCaAHvua90yhmpzzV6fF4zOhwkGGbPCgUWmKyJ83uz+DXaPdYxX0ttDvtolrzx3Q=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-J+qz4Al05PrNIOdj7xsWVTyx0c/gjUauG5nKV3Rrx0Q+5JO+1pPVlnfNmWbOF9pKG4f3IGad8KXJUfGMORld+Q=="], + + "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UOdkwScHRkGPz+n9ZJU7sTkTvqV7rD1SLCLaru1xH8WRsV7tDorPqNCzEN1msOIiPRK825nvAtEm9UsomO1GsA=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-E51tyWDP1l0CbjZYhiUxhDGPaY8Hf5YBREx0PHBff1LM1/q3qsJ6ZvRUa8YbbOO0Ax9QP6GHjD9vf3n6bXZ7QA=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-cCsXK9AQ9Zf18QlVnbrFu2IKfr4sf2sfbErkF2jfCzyCO9Bnhl0KRx63zlN+Ni1xU7gcBLAssgcui5R400N2eA=="], + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], @@ -168,8 +298,20 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@stryker-mutator/api": ["@stryker-mutator/api@9.6.0", "", { "dependencies": { "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-kJEEwOVoWDXGEIXuM+9efT6LSJ7nyxnQQvjEoKg8GSZXbDUjfD0tqA0aBD06U1SzQLKCM7ffjgPffr154MHZKw=="], + + "@stryker-mutator/core": ["@stryker-mutator/core@9.6.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/instrumenter": "9.6.0", "@stryker-mutator/util": "9.6.0", "ajv": "~8.18.0", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.2.4", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.7.2", "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.2.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-oSbw01l6HXHt0iW9x5fQj7yHGGT8ZjCkXSkI7Bsu0juO7Q6vRMXk7XcvKpCBgRgzKXi1osg8+iIzj7acHuxepQ=="], + + "@stryker-mutator/instrumenter": ["@stryker-mutator/instrumenter@9.6.0", "", { "dependencies": { "@babel/core": "~7.29.0", "@babel/generator": "~7.29.0", "@babel/parser": "~7.29.0", "@babel/plugin-proposal-decorators": "~7.29.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.28.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "angular-html-parser": "~10.4.0", "semver": "~7.7.0", "tslib": "2.8.1", "weapon-regex": "~1.3.2" } }, "sha512-tWdRYfm9LF4Go7cNOos0xEIOEnN7ZOSj38rfXvGZS9IINlvYBrBCl2xcz/67v6l5A7xksMWWByZRIq2bgdnnUg=="], + + "@stryker-mutator/util": ["@stryker-mutator/util@9.6.0", "", {}, "sha512-gw7fJOFNHEj9inAEOodD9RrrMEMhZmWJ46Ww/kDJAXlSsBBmdwCzeomNLngmLTvgp14z7Tfq85DHYwvmNMdOxA=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], @@ -180,6 +322,8 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], @@ -242,9 +386,7 @@ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], @@ -274,14 +416,20 @@ "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun": ["bun@1.3.11", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.11", "@oven/bun-darwin-x64": "1.3.11", "@oven/bun-darwin-x64-baseline": "1.3.11", "@oven/bun-linux-aarch64": "1.3.11", "@oven/bun-linux-aarch64-musl": "1.3.11", "@oven/bun-linux-x64": "1.3.11", "@oven/bun-linux-x64-baseline": "1.3.11", "@oven/bun-linux-x64-musl": "1.3.11", "@oven/bun-linux-x64-musl-baseline": "1.3.11", "@oven/bun-windows-aarch64": "1.3.11", "@oven/bun-windows-x64": "1.3.11", "@oven/bun-windows-x64-baseline": "1.3.11" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-AvXWYFO6j/ZQ7bhGm4X6eilq2JHsDVC90ZM32k2B7/srhC2gs3Sdki1QTbwrdRCo8o7eT+167vcB1yzOvPdbjA=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -292,18 +440,20 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -342,8 +492,12 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -352,6 +506,10 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], @@ -374,19 +532,21 @@ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], + "eslint": ["eslint@10.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA=="], "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], @@ -400,6 +560,8 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -412,8 +574,18 @@ "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -428,6 +600,8 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], @@ -448,12 +622,18 @@ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], @@ -466,8 +646,6 @@ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], @@ -480,12 +658,12 @@ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -524,6 +702,8 @@ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], @@ -534,12 +714,16 @@ "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], @@ -552,18 +736,26 @@ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], @@ -578,6 +770,8 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], @@ -590,8 +784,6 @@ "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -600,6 +792,8 @@ "lossless-json": ["lossless-json@2.0.1", "", {}, "sha512-KW/FSL426qblKVvf4ImeMVGr0Je6J9aXvAMUOIU8AzelDj06q47mn6QJ+56lBBd+A8kjrncrxdKQs6ZssAXTmw=="], + "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -618,10 +812,24 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mutation-server-protocol": ["mutation-server-protocol@0.4.1", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g=="], + + "mutation-testing-elements": ["mutation-testing-elements@3.7.2", "", {}, "sha512-i7X2Q4X5eYon72W2QQ9HND7plVhQcqTnv+Xc3KeYslRZSJ4WYJoal8LFdbWm7dKWLNE0rYkCUrvboasWzF3MMA=="], + + "mutation-testing-metrics": ["mutation-testing-metrics@3.7.2", "", { "dependencies": { "mutation-testing-report-schema": "3.7.2" } }, "sha512-ichXZSC4FeJbcVHYOWzWUhNuTJGogc0WiQol8lqEBrBSp+ADl3fmcZMqrx0ogInEUiImn+A8JyTk6uh9vd25TQ=="], + + "mutation-testing-report-schema": ["mutation-testing-report-schema@3.7.2", "", {}, "sha512-fN5M61SDzIOeJyatMOhGPLDOFz5BQIjTNPjo4PcHIEUWrejO4i4B5PFuQ/2l43709hEsTxeiXX00H73WERKcDw=="], + + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "mysql2": ["mysql2@3.18.2", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw=="], "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], @@ -634,6 +842,10 @@ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -662,7 +874,9 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -674,6 +888,8 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -688,6 +904,10 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -708,14 +928,16 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -754,6 +976,10 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], @@ -776,7 +1002,9 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "stryker-mutator-bun-runner": ["stryker-mutator-bun-runner@0.4.0", "", { "dependencies": { "@stryker-mutator/api": "^9.0.1", "@stryker-mutator/util": "^9.0.1", "execa": "^9.6.0", "glob": "^11.0.3", "semver": "^7.5.4" }, "peerDependencies": { "@stryker-mutator/core": "^9.0.0", "bun": ">=1.0.0" } }, "sha512-wx2nfdfKFWRxE5fiAYzRBVJoUMtoeYIE2uAtWS1fu4rGeay6XVm3wBmqi2bKosVH6zHPShsciSCDKDGifZHvSA=="], "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], @@ -796,8 +1024,14 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], @@ -810,16 +1044,26 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="], + + "typed-rest-client": ["typed-rest-client@2.2.0", "", { "dependencies": { "des.js": "^1.1.0", "js-md4": "^0.3.2", "qs": "^6.14.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-/e2Rk9g20N0r44kaQLb3v6QGuryOD8SPb53t43Y5kqXXA+SqWuU7zLiMxetw61jNn/JFrxTdr5nPDhGY/eTNhQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -828,6 +1072,8 @@ "vitest": ["vitest@4.1.1", "", { "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", "@vitest/pretty-format": "4.1.1", "@vitest/runner": "4.1.1", "@vitest/snapshot": "4.1.1", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.1", "@vitest/browser-preview": "4.1.1", "@vitest/browser-webdriverio": "4.1.1", "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA=="], + "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -844,30 +1090,44 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@stryker-mutator/core/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@stryker-mutator/core/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "@stryker-mutator/instrumenter/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "stryker-mutator-bun-runner/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@stryker-mutator/core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/app/server/services/ts/user/integration/goals.test.ts b/app/server/services/ts/user/integration/goals.test.ts new file mode 100644 index 0000000..77404ba --- /dev/null +++ b/app/server/services/ts/user/integration/goals.test.ts @@ -0,0 +1,238 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import request from "supertest"; +import { setupUserWithGoals } from "./setup.ts"; +import type { Goal } from "../src/types/Goals.ts"; + +const BASE_URL = process.env.API_GATEWAY_ADDR; + +let token: string; + +beforeAll(async () => { + const setup = await setupUserWithGoals(BASE_URL, "goals-test", [ + { + name: "Reduce Grocery Spending", + type: "reduce_spending", + category: "groceries", + target: 400, + period: "m", + }, + { + type: "save", + name: "New Goal", + category: "Unknown", + target: 100, + period: "m", + }, + ]); + token = setup.token; +}, 30000); + +describe("Goals Integration (Docker)", () => { + let createdGoalId: number; + + it("creates a new spending reduction goal", async () => { + const goalData = { + name: "Reduce Grocery Spending", + type: "reduce_spending", + category: "groceries", + target: 400, + period: "m", + }; + + const res = await request(BASE_URL) + .post("/goals") + .set("Authorization", `Bearer ${token}`) + .send(goalData); + + expect(res.status).toBe(201); + expect(res.body).toHaveProperty("id"); + expect(res.body.name).toBe(goalData.name); + expect(res.body.type).toBe(goalData.type); + expect(res.body.target).toBe("400.00"); + expect(res.body.period).toBe(goalData.period); + + createdGoalId = res.body.id; + }); + + it("creates a savings goal", async () => { + const goalData = { + type: "save", + name: "New Goal", + category: "Unknown", + target: 100, + period: "m", + }; + + const res = await request(BASE_URL) + .post("/goals") + .set("Authorization", `Bearer ${token}`) + .send(goalData); + + expect(res.status).toBe(201); + expect(res.body.name).toBe(goalData.name); + expect(res.body.type).toBe(goalData.type); + expect(res.body.target).toBe("100.00"); + }); + + it("lists all goals for the user", async () => { + const res = await request(BASE_URL) + .get("/goals") + .set("Authorization", `Bearer ${token}`); + + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + }); + + it("updates a goal (using query parameter gid)", async () => { + const updates = { + id: createdGoalId, + name: "Updated Grocery Goal", + profile_id: 1, + target: 350, + }; + + const res = await request(BASE_URL) + .patch(`/goals?gid=${createdGoalId}`) + .set("Authorization", `Bearer ${token}`) + .send(updates); + + expect(res.status).toBe(200); + expect(res.body.name).toBe(updates.name); + expect(res.body.target).toBe("350.00"); + }); + + it("deletes a goal (using query parameter gid)", async () => { + // Create a temporary goal to delete + const createRes = await request(BASE_URL) + .post("/goals") + .set("Authorization", `Bearer ${token}`) + .send({ + name: "Temp Goal", + type: "save", + category: "temp", + target: 100, + period: "m", + }); + + const tempGoalId = createRes.body.id; + + const deleteRes = await request(BASE_URL) + .delete(`/goals?gid=${tempGoalId}`) + .set("Authorization", `Bearer ${token}`); + + expect(deleteRes.status).toBe(204); + + // Verify it's gone - should return 404 + const getRes = await request(BASE_URL) + .get("/goals") + .set("Authorization", `Bearer ${token}`); + + const stillExists = getRes.body.some( + (goal: Goal) => goal.id === tempGoalId, + ); + expect(stillExists).toBe(false); + }); + + it("respects the 5-goal limit", async () => { + // First, delete all existing goals to start fresh + const listRes = await request(BASE_URL) + .get("/goals") + .set("Authorization", `Bearer ${token}`); + + for (const goal of listRes.body) { + const result = await request(BASE_URL) + .delete(`/goals?gid=${goal.id}`) + .set("Authorization", `Bearer ${token}`); + + expect(result.status).toBe(204); + } + + const listResPostDelete = await request(BASE_URL) + .get("/goals") + .set("Authorization", `Bearer ${token}`); + + expect(listResPostDelete.body.length).toBe(0); + + // Create 5 goals + for (let i = 0; i < 5; i++) { + const res = await request(BASE_URL) + .post("/goals") + .set("Authorization", `Bearer ${token}`) + .send({ + name: `Goal ${i}`, + type: "save", + category: `category${i}`, + target: 100, + period: "m", + }); + expect(res.status).toBe(201); + } + + // Try to create a 6th + const res = await request(BASE_URL) + .post("/goals") + .set("Authorization", `Bearer ${token}`) + .send({ + name: "Sixth Goal", + profile_id: 1, + type: "save", + category: "extra", + target: 100, + period: "m", + }); + + expect(res.status).toBe(400); + expect(res.body.error).toBe("Maximum 5 goals allowed per profile"); + }); + + it("rejects goal creation without required fields", async () => { + // Missing period for reduce_spending + const res = await request(BASE_URL) + .post("/goals") + .set("Authorization", `Bearer ${token}`) + .send({ + name: "Invalid Goal", + type: "reduce_spending", + category: "groceries", + target: 400, + // missing period + }); + + expect(res.status).toBe(400); + expect(res.body.error).toBe("Missing required fields"); + }); + + it("rejects goal creation without auth", async () => { + const res = await request(BASE_URL).post("/goals").send({ + name: "Unauthorized Goal", + profile_id: 1, + type: "save", + category: "test", + target: 100, + period: "m", + }); + + expect(res.status).toBe(401); + }); + + it("rejects update without gid parameter", async () => { + const res = await request(BASE_URL) + .patch("/goals") + .set("Authorization", `Bearer ${token}`) + .send({ + name: "Should Fail", + }); + + // Without gid, should return 400 or 404 + expect(res.status).toBe(404); + }); + + it("rejects delete without gid parameter", async () => { + const res = await request(BASE_URL) + .delete("/goals") + .set("Authorization", `Bearer ${token}`); + + expect(res.status).toBe(404); + }); +}); diff --git a/app/server/services/ts/user/integration/setup.ts b/app/server/services/ts/user/integration/setup.ts index 5ba099a..83f9098 100644 --- a/app/server/services/ts/user/integration/setup.ts +++ b/app/server/services/ts/user/integration/setup.ts @@ -84,3 +84,86 @@ export async function createAuthenticatedAccount( throw new Error(`Timed out creating authenticated account: ${lastFailure}`); } + +export interface GoalData { + name: string; + type: "save" | "reduce_spending"; + category: string; + target: number; + period?: "w" | "m"; +} + +export async function createGoal( + baseUrl: string, + token: string, + goalData: GoalData, +) { + const response = await request(baseUrl) + .post("/goals") + .set("Authorization", `Bearer ${token}`) + .send(goalData); + + if (response.status !== 201) { + throw new Error( + `Failed to create goal: ${response.status} ${JSON.stringify(response.body)}`, + ); + } + + return response.body; +} + +export async function createMultipleGoals( + baseUrl: string, + token: string, + goalsData: GoalData[], +) { + const createdGoals = []; + for (const goalData of goalsData) { + const goal = await createGoal(baseUrl, token, goalData); + createdGoals.push(goal); + } + return createdGoals; +} + +export async function deleteAllGoals(baseUrl: string, token: string) { + // Fetch all goals + const listRes = await request(baseUrl) + .get("/goals") + .set("Authorization", `Bearer ${token}`); + + if (listRes.status === 200 && Array.isArray(listRes.body)) { + // Delete each goal + for (const goal of listRes.body) { + await request(baseUrl) + .delete(`/goals?gid=${goal.id}`) + .set("Authorization", `Bearer ${token}`); + } + } +} + +export async function setupUserWithGoals( + baseUrl: string, + prefix: string, + goalsData: GoalData[], +) { + // First create the user account + const { token } = await createAuthenticatedAccount(baseUrl, prefix, { + name: "Test Account", + type: "chequing", + balance: 5000, + value: 5000, + subtype: "na", + }); + + // Clean up any existing goals (in case of leftover data) + await deleteAllGoals(baseUrl, token); + + // Create the specified goals + const createdGoals = await createMultipleGoals(baseUrl, token, goalsData); + + return { + token, + goals: createdGoals, + deleteAllGoals: () => deleteAllGoals(baseUrl, token), + }; +} diff --git a/app/server/services/ts/user/package.json b/app/server/services/ts/user/package.json index 4c7ec47..837e93e 100644 --- a/app/server/services/ts/user/package.json +++ b/app/server/services/ts/user/package.json @@ -12,7 +12,7 @@ "@types/supertest": "^7.2.0", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", - "eslint": "^9.39.2", + "eslint": "^10.1.0", "eslint-plugin-react": "^7.37.5", "globals": "^17.3.0", "jiti": "^2.6.1", @@ -25,7 +25,9 @@ "scripts": { "dev": "bun --watch src/index.ts", "build": "tsc", - "start": "bun dist/index.js" + "start": "bun dist/index.js", + "test:mutation": "npx stryker run", + "test": "vitest" }, "dependencies": { "@mysql/xdevapi": "^8.0.35", @@ -33,6 +35,8 @@ "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mysql2": "^3.15.3", - "vitest": "^4.1.1" + "stryker-mutator-bun-runner": "^0.4.0", + "supertest": "^7.2.2", + "vitest": "^4.0.18" } } diff --git a/app/server/services/ts/user/reports/mutation/mutation.html b/app/server/services/ts/user/reports/mutation/mutation.html new file mode 100644 index 0000000..e67e91a --- /dev/null +++ b/app/server/services/ts/user/reports/mutation/mutation.html @@ -0,0 +1,11350 @@ + + + + + + + + + + + + + + + + + + + Your browser doesn't support +
custom elements. Please use a latest version of an evergreen browser (Firefox, Chrome, + Safari, Opera, Edge, etc). + + + + diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index d05a3f9..31302da 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -9,31 +9,18 @@ import { getSnapshotData } from "./logic/snapshot.ts"; import { onExit } from "@/hooks"; import { buildCorsConfig } from "@/expressUtils"; import { PORT } from "@/port"; - -// import express from "express"; +import { + deleteUserGoal, + createUserGoal, + updateUserGoal, + getUserGoals, + getUserGoalById, +} from "./logic/goals.ts"; +import type { GoalType, UpdateGoalInput } from "./types/Goals.ts"; +import { getGoalCountByProfileId } from "./queries/goals.ts"; import { accountsRouter } from "./routes/account.js"; import { profilesRouter } from "./routes/profile.js"; import { transactionsRouter } from "./routes/transaction.js"; -// import { PORT } from "@/port"; -// import { onExit } from "@/hooks"; -// import { buildCorsConfig } from "@/expressUtils"; -// import { getExpensesChartData } from "./logic/expenses.ts"; -// import { authenticateJWT } from "./utils/auth.ts"; -// import { pool } from "./db.ts"; -// import type { Request } from "express"; -// import type { RowDataPacket } from "mysql2/promise"; -// import mysql from "mysql2/promise"; -// import jwt from "jsonwebtoken"; - -// const pool = mysql.createPool({ -// host: process.env.MYSQL_HOST, -// port: Number(process.env.MYSQL_PORT), -// user: process.env.MYSQL_USER, -// password: process.env.MYSQL_PASSWORD, -// database: process.env.DB_NAME, -// }); - -// const JWT_SECRET = process.env.JWT_SECRET; const app = express(); app.use(buildCorsConfig()); @@ -41,49 +28,13 @@ onExit(async () => await server.close()); app.use(express.json()); -app.use((req, res, next) => { - console.log("USER Incoming request: " + req.method + " " + req.url); - console.log(req.body); - next(); -}); - -/* -function verifyToken( - req: express.Request, - res: express.Response, - next: NextFunction, -) { - const authHeader = req.headers.authorization; - console.log(authHeader) - //Determine if jwt header was passed - if (authHeader) { - const token = authHeader.split(" ")[1]; - try { - const payload = jwt.verify(token, process.env.JWT_SECRET); - - console.log(payload); - - next(); - } catch (err) { - console.error(err); - res.status(401).json({ error: "Invalid token" }); - } - } else { - res - .status(401) - .json({ - error: "Failed to pass token, user not authorized to send request", - }); - } -} -*/ - -//app.use(verifyToken); - -//test endpoint -// app.get("/health", (req: express.Request, res: express.Response) => { -// res.send("ok"); -// }); +app.use( + (req: express.Request, res: express.Response, next: express.NextFunction) => { + // console.log("USER Incoming request: " + req.method + " " + req.url); + // console.log(req.body); + next(); + }, +); app.use("/accounts", accountsRouter); app.use("/transactions", transactionsRouter); @@ -94,442 +45,249 @@ const server = app.listen(PORT, () => { }); process.on("SIGTERM", () => server.close()); -// interface Transaction extends RowDataPacket { -// id: number; -// financialAccount_id: string; -// amount: number; -// category: string; -// description: string; -// sender: string; -// recipient: string; -// date: string; -// } - -//authenticaion of JWT - returns user id -// export const authenticateJWT = (req: Request) => { -// const authHeader = req.headers.authorization; -// console.log(req.headers); -// if (!authHeader) { -// throw new Error("Authorization header missing"); -// } - -// const parts = authHeader.split(" "); -// if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { -// throw new Error("Invalid authorization header format"); -// } -// const token = parts[1]; - -// const decoded = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload; -// const userId = decoded.sub; -// //console.log("user id: " + userId); -// if (!userId) { -// throw new Error("User ID not found in token"); -// } - -// return userId; -// }; - -// //interface for expense data so that rows of the query result can be typed and traversed -// interface ExpenseRow { -// label: string; -// total_expenses: number; -// date_group?: string; -// } - -app.get("/charts/expenses", async (req, res) => { +app.get( + "/charts/expenses", + async (req: express.Request, res: express.Response) => { + try { + const userId = authenticateJWT(req); + const period = req.query.period as string; + + if (!validatePeriod(period)) { + return res + .status(400) + .json({ error: "Invalid period. Must be 'w', 'm', or 'y'." }); + } + + const chartData = await getExpensesChartData(pool, userId, period); + res.json(chartData); + } catch (error) { + console.error("Error fetching expenses chart data:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ error: "Failed to fetch expenses chart data" }); + } + } + }, +); + +app.get( + "/table/transactions", + async (req: express.Request, res: express.Response) => { + try { + const userId = authenticateJWT(req); + const transactions = await getTransactionsData(pool, userId); + + if (!transactions || transactions.length === 0) { + return res.json([]); // Return empty array for no transactions + } + + res.json(transactions); + } catch (error) { + console.error("Error fetching transactions:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ error: "Failed to fetch transactions" }); + } + } + }, +); + +app.get( + "/table/snapshot", + async (req: express.Request, res: express.Response) => { + try { + const userId = authenticateJWT(req); + + if (!userId) { + return res.status(401).json({ error: "User not authenticated" }); + } + + const snapshotData = await getSnapshotData(pool, userId); + res.json(snapshotData); + } catch (error) { + console.error("Error fetching snapshot data:", error); + if (error instanceof Error) { + if (error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else if (error.message.includes("No snapshot data")) { + res.status(404).json({ error: "No snapshot data found" }); + } else { + res.status(500).json({ error: "Failed to fetch snapshot data" }); + } + } else { + res.status(500).json({ error: "Failed to fetch snapshot data" }); + } + } + }, +); + +export { generateDateRange }; //for testing purposes only + +// Goal stuff is below------------------------------------------------------ + +// GET all goals +app.get("/goals", async (req: express.Request, res: express.Response) => { try { const userId = authenticateJWT(req); - const period = req.query.period as string; - if (!validatePeriod(period)) { + const goals = await getUserGoals(pool, userId); + + res.json(goals); + } catch (error) { + console.error("Error fetching goals:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ error: "Failed to fetch goals" }); + } + } +}); + +// POST create new goal +app.post("/goals", async (req: express.Request, res: express.Response) => { + try { + // const userId = authenticateJWT(req); + // const name = req.body.name; + // const type = req.body.type; + // const category = req.body.category; + // const target = req.body.target; + // const period = req.body.period; + + const userId = authenticateJWT(req); + let name = null; + let type = null; + let category = null; + let target = null; + let period = null; + + if (req.body.name) name = req.body.name; + if (req.body.type) type = req.body.type; + if (req.body.category) category = req.body.category; + if (req.body.target) target = req.body.target; + if (req.body.period) period = req.body.period; + + if (!name || !type || !target || !period) { + return res.status(400).json({ error: "Missing required fields" }); + } + //check goal limit (max 5) + const goalCount = await getGoalCountByProfileId(pool, userId); + if (goalCount >= 5) { return res .status(400) - .json({ error: "Invalid period. Must be 'w', 'm', or 'y'." }); + .json({ error: "Maximum 5 goals allowed per profile" }); } - const chartData = await getExpensesChartData(pool, userId, period); - res.json(chartData); + const newGoal = await createUserGoal(pool, userId, { + name, + type, + category, + target, + period, + }); + res.status(201).json(newGoal); } catch (error) { - console.error("Error fetching expenses chart data:", error); + console.error("Error creating goal:", error); if (error instanceof Error && error.message.includes("Authorization")) { res.status(401).json({ error: error.message }); } else { - res.status(500).json({ error: "Failed to fetch expenses chart data" }); + res.status(500).json({ error: "Failed to create goal" }); } } }); -app.get("/table/transactions", async (req, res) => { +// PATCH update goal +app.patch("/goals", async (req: express.Request, res: express.Response) => { try { const userId = authenticateJWT(req); - const transactions = await getTransactionsData(pool, userId); + // const goalId = parseInt(req.query.gid); + let goalId = null; + + if (req.query.gid) goalId = parseInt(req.query.gid); + + if (!goalId) { + return res.status(404).json({ error: "Missing goal ID" }); + } - if (!transactions || transactions.length === 0) { - return res.json([]); // Return empty array for no transactions + //verify goal exists and belongs to user + const existingGoal = await getUserGoalById(pool, goalId, userId); + if (!existingGoal) { + return res.status(404).json({ error: "Goal not found" }); } - res.json(transactions); + //build updates object with only provided fields - for security + const updates: UpdateGoalInput = {}; + if (req.body.name !== undefined) updates.name = req.body.name; + if (req.body.category !== undefined) + updates.category = req.body.category.toLowerCase(); + if (req.body.target !== undefined) updates.target = req.body.target; + if (req.body.period !== undefined) { + updates.period = req.body.period; + } //period can be 'na' if goal is savings + if (req.body.type !== undefined) updates.type = req.body.type; + + //check that period exists if the goal type is reduce_spending, it doesn't matter if type is 'save' because period is ignored + if ( + updates.type && + updates.type === ("reduce_spending" as GoalType) && + !updates.period + ) { + return res + .status(400) + .json({ error: "Period is required for reduce_spending goals" }); + } + + //validate updates + if (updates.target !== undefined && updates.target <= 0) { + return res + .status(400) + .json({ error: "Target amount must be greater than 0" }); + } + + const updatedGoal = await updateUserGoal(pool, goalId, userId, updates); + + if (!updatedGoal) { + return res.status(404).json({ error: "Goal not found" }); + } + + res.json(updatedGoal); } catch (error) { - console.error("Error fetching transactions:", error); + console.error("Error updating goal:", error); if (error instanceof Error && error.message.includes("Authorization")) { res.status(401).json({ error: error.message }); } else { - res.status(500).json({ error: "Failed to fetch transactions" }); + res.status(500).json({ error: "Failed to update goal" }); } } }); -app.get("/table/snapshot", async (req, res) => { +// DELETE goal +app.delete("/goals", async (req: express.Request, res: express.Response) => { try { const userId = authenticateJWT(req); + // const goalId = parseInt(req.query.gid); + + let goalId = null; + + if (req.query.gid) goalId = parseInt(req.query.gid); + + if (!goalId) { + return res.status(404).json({ error: "Missing goal ID" }); + } - if (!userId) { - return res.status(401).json({ error: "User not authenticated" }); + const deleted = await deleteUserGoal(pool, goalId, userId); + if (!deleted) { + return res.status(404).json({ error: "Goal not found" }); } - const snapshotData = await getSnapshotData(pool, userId); - res.json(snapshotData); + res.status(204).send(); } catch (error) { - console.error("Error fetching snapshot data:", error); - if (error instanceof Error) { - if (error.message.includes("Authorization")) { - res.status(401).json({ error: error.message }); - } else if (error.message.includes("No snapshot data")) { - res.status(404).json({ error: "No snapshot data found" }); - } else { - res.status(500).json({ error: "Failed to fetch snapshot data" }); - } + console.error("Error deleting goal:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); } else { - res.status(500).json({ error: "Failed to fetch snapshot data" }); + res.status(500).json({ error: "Failed to delete goal" }); } } }); - -export { generateDateRange }; //for testing purposes only - -// app.get( -// "/charts/expenses", -// async (req: express.Request, res: express.Response) => { -// try { -// const userId = authenticateJWT(req); -// // console.log("User authenticated with ID:", userId); -// const period = req.query.period as string; -// const connection = await pool.getConnection(); - -// if (!["w", "m", "y"].includes(period)) { -// return res -// .status(400) -// .json({ error: "Invalid period. Must be 'w', 'm', or 'y'." }); -// } -// const endDate = new Date(); -// const startDate = new Date(); -// let dateFormat: string = "%Y-%m-%d"; -// let selectFormat: string; - -// switch (period) { -// case "w": -// startDate.setDate(endDate.getDate() - 7); -// dateFormat = "%Y-%m-%d"; -// selectFormat = "DATE(date)"; -// break; -// case "m": -// startDate.setDate(endDate.getDate() - 30); -// dateFormat = "%Y-%m-%d"; -// selectFormat = "DATE(date)"; -// break; -// case "y": -// startDate.setDate(endDate.getDate() - 365); -// dateFormat = "%Y-%m"; -// selectFormat = 'DATE_FORMAT(date, "%Y-%m-01")'; //first day of month for grouping -// break; -// default: -// return res -// .status(400) -// .json({ error: "Invalid period. Must be 'w', 'm', or 'y'." }); -// } - -// const startDateStr = startDate.toISOString().slice(0, 10); // YYYY-MM-DD -// const endDateStr = endDate.toISOString().slice(0, 10); - -// //grabs all transactions that are less than 0 in amount -// const query = ` -// SELECT -// ${selectFormat} as date_group, -// DATE_FORMAT(t.date, ?) as label, -// SUM(ABS(t.amount)) as total_expenses -// FROM finus.transaction t -// JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// JOIN finus.finusAccount u ON uap.account_id = u.id -// WHERE t.amount < 0 -// AND u.id = ? -// AND t.date >= ? -// AND t.date <= ? -// GROUP BY date_group, DATE_FORMAT(t.date, ?) -// ORDER BY date_group ASC -// `; - -// const [rows] = await connection.query(query, [ -// dateFormat, -// userId, -// startDateStr, -// endDateStr, -// dateFormat, -// ]); -// const expenses = rows as ExpenseRow[]; -// connection.release(); - -// //makes a complete date range even with days of no transactions -// const allLabels = generateDateRange(startDate, endDate, period); -// const dataMap = new Map(); - -// if (Array.isArray(expenses)) { -// expenses.forEach((row) => { -// dataMap.set(row.label, Number(row.total_expenses)); -// }); -// } - -// const data = allLabels.map((label) => dataMap.get(label) || 0); - -// const periodLabels = { -// w: "Weekly Expenses", -// m: "Monthly Expenses", -// y: "Yearly Expenses", -// }; -// //console.log("Found data:", data); -// res.json({ -// labels: allLabels, -// datasets: [ -// { -// label: periodLabels[period as keyof typeof periodLabels], -// data, -// }, -// ], -// }); -// } catch (error) { -// console.error("Error fetching expenses chart data:", error); -// res.status(500).json({ error: "Failed to fetch expenses chart data" }); -// } -// }, -// ); - -//this gets all transactions for now - can be capped to a certain amount in the future when any user reaches over 100k transactions -// app.get( -// "/table/trasactions", -// async (req: express.Request, res: express.Response) => { -// try { -// //console.log("Fetching transactions..."); -// const userId = authenticateJWT(req); -// const connection = await pool.getConnection(); -// const query = ` -// SELECT * -// FROM finus.transaction t -// JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// JOIN finus.finusAccount u ON uap.account_id = u.id -// WHERE u.id = ? -// ORDER BY t.date DESC -// `; - -// const [rows] = await connection.query(query, [userId]); -// connection.release(); - -// //get first and last names of the user associated with the first transaction -// const first_name = rows[0] ? rows[0].first_name : ""; -// const last_name = rows[0] ? rows[0].last_name : ""; - -// if (Array.isArray(rows)) { -// rows.forEach((row: Transaction) => { -// if (!row.sender) { -// //these cases really only appear because of the populator script -// row.sender = first_name + " " + last_name; -// } -// if (!row.recipient) { -// row.recipient = first_name + " " + last_name; -// } -// }); -// } -// res.json(rows); -// } catch (error) { -// console.error("Error fetching transactions:", error); -// res.status(500).json({ error: "Failed to fetch transactions" }); -// } -// }, -// ); - -// interface SnapshotRow extends RowDataPacket { -// total_balance: number; -// current_income: number; -// total_expenses: number; -// transaction_count: number; -// current_debt: number; -// total_savings: number; -// } - -//gets the following totals as massively aggregated values: -// total balance - combined sum of all account balances -// current income - YTD sum of all positive transactions -// average expenses - YTD average of all negative transactions -// current debt - sum of all credit_card accounts of subtype 'loan' -// total savings - sum of all savings accounts -// app.get( -// "/table/snapshot", -// async (req: express.Request, res: express.Response) => { -// let connection; -// try { -// const userId = authenticateJWT(req); -// if (!userId) { -// return res.status(401).json({ error: "User not authenticated" }); -// } - -// connection = await pool.getConnection(); - -// const today = new Date(); -// const ytdStart = new Date(today.getFullYear(), 0, 1); -// const ytdStartStr = ytdStart.toISOString().slice(0, 10); -// const todayStr = today.toISOString().slice(0, 10); -// const monthsPassed = today.getMonth() + 1; -// //single massive query to get all the data - this is apparently more efficient than multiple queries -// const [results] = await connection.query( -// ` -// SELECT -// -- Total Balance -// (SELECT COALESCE(SUM(fa.balance), 0) -// FROM finus.financialAccount fa -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// WHERE uap.account_id = ?) as total_balance, - -// -- Current Income (YTD) -// (SELECT COALESCE(SUM(t.amount), 0) -// FROM finus.transaction t -// JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// WHERE uap.account_id = ? -// AND t.amount > 0 -// AND t.date BETWEEN ? AND ?) as current_income, - -// -- Total Expenses (YTD) -// (SELECT COALESCE(SUM(ABS(t.amount)), 0) -// FROM finus.transaction t -// JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// WHERE uap.account_id = ? -// AND t.amount < 0 -// AND t.date BETWEEN ? AND ?) as total_expenses, - -// -- Transaction Count (for averaging) -// (SELECT COUNT(*) -// FROM finus.transaction t -// JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// WHERE uap.account_id = ? -// AND t.amount < 0 -// AND t.date BETWEEN ? AND ?) as transaction_count, - -// -- Current Debt (credit_card with loan subtype) -// (SELECT COALESCE(SUM(fa.balance), 0) -// FROM finus.financialAccount fa -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// WHERE uap.account_id = ? -// AND fa.type = 'credit_card' -// AND fa.subtype = 'loan') as current_debt, - -// -- Total Savings -// (SELECT COALESCE(SUM(fa.balance), 0) -// FROM finus.financialAccount fa -// JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id -// JOIN finus.profile p ON pfa.profile_id = p.id -// JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id -// WHERE uap.account_id = ? -// AND fa.type = 'savings') as total_savings -// `, -// [ -// userId, -// userId, -// ytdStartStr, -// todayStr, // for total_balance and current_income -// userId, -// ytdStartStr, -// todayStr, // for total_expenses -// userId, -// ytdStartStr, -// todayStr, // for transaction_count -// userId, // for current_debt -// userId, // for total_savings -// ], -// ); - -// connection.release(); - -// const data = results[0]; -// const avgMonthlyExpenses = -// monthsPassed > 0 ? data.total_expenses / monthsPassed : 0; -// const response = { -// totalBalance: data.total_balance || 0, -// currentIncome: data.current_income || 0, -// averageExpenses: Math.round(avgMonthlyExpenses * 100) / 100, -// currentDebt: data.current_debt || 0, -// totalSavings: data.total_savings || 0, -// metadata: { -// ytdPeriod: { -// start: ytdStartStr, -// end: todayStr, -// }, -// monthsInYTD: monthsPassed, -// transactionCount: data.transaction_count || 0, -// }, -// }; - -// res.json(response); -// } catch (error) { -// console.error("Error fetching snapshot data:", error); -// if (connection) { -// connection.release(); -// } -// res.status(500).json({ error: "Failed to fetch snapshot data" }); -// } -// }, -// ); - -//helper method for making a complete date range -// function generateDateRange(start: Date, end: Date, period: string): string[] { -// const dates: string[] = []; -// const current = new Date(start); - -// current.setHours(0, 0, 0, 0); -// const endDate = new Date(end); -// endDate.setHours(0, 0, 0, 0); - -// while (current <= endDate) { -// if (period === "y") { -// //monthly labels for year period -// const year = current.getFullYear(); -// const month = String(current.getMonth() + 1).padStart(2, "0"); -// dates.push(`${year}-${month}`); -// current.setMonth(current.getMonth() + 1); -// } else { -// //day-basis labels for everything else - week and month -// const year = current.getFullYear(); -// const month = String(current.getMonth() + 1).padStart(2, "0"); -// const day = String(current.getDate()).padStart(2, "0"); -// dates.push(`${year}-${month}-${day}`); -// current.setDate(current.getDate() + 1); -// } -// } - -// return dates; -// } diff --git a/app/server/services/ts/user/src/logic/goals.ts b/app/server/services/ts/user/src/logic/goals.ts new file mode 100644 index 0000000..5fd2f97 --- /dev/null +++ b/app/server/services/ts/user/src/logic/goals.ts @@ -0,0 +1,260 @@ +import { Pool } from "mysql2/promise"; +import * as goalsQueries from "../queries/goals.ts"; +import * as transactionsQueries from "../queries/transactions.ts"; +import type { + Goal, + CreateGoalInput, + UpdateGoalInput, + GoalWithProgress, +} from "../types/Goals.ts"; + +const MAX_GOALS_PER_PROFILE = 5; + +//get current spending/achievement for a goal based on transactions +export async function calculateCurrentAmount( + pool: Pool, + goal: Goal, + profileId: number | null, +): Promise { + if (!profileId) { + // console.log("No profileId provided"); + // throw new Error("No profileId provided"); + return 0; + } + + if (goal.type === "reduce_spending") { + const period = goal.period; + const now = new Date(); + let startDate: Date; + + if (period === "w") { + // Start of week (Sunday) + startDate = new Date(now); + startDate.setDate(now.getDate() - now.getDay()); + startDate.setHours(0, 0, 0, 0); + // console.log(`Weekly period: ${startDate.toISOString()} to ${now.toISOString()}`); + } else { + // Start of month + startDate = new Date(now.getFullYear(), now.getMonth(), 1); + startDate.setHours(0, 0, 0, 0); + // console.log(`Monthly period: ${startDate.toISOString()} to ${now.toISOString()}`); + } + + //get expenses (negative amounts) for this category in the period + const expenses = await transactionsQueries.getDateCategoryTransactionsQuery( + pool, + profileId, + goal.category!, + startDate, + now, + ); + + //sum the amounts (they are negative, so sum will be negative) + const totalSpending = expenses.reduce( + (total, transaction) => total + transaction.amount, + 0, + ); + + //return absolute value for display + // console.log(`Total spending for ${goal.category}: ${totalSpending}`); + return Math.abs(totalSpending); + } else if (goal.type === "save") { + // For savings goals, get all transactions for this category + const transactions = + await transactionsQueries.getProfileCategoryTransactionsQuery( + pool, + profileId, + goal.category!, + ); + + //sum all amounts (positive = savings, negative = expenses) + const total = transactions.reduce( + (sum, transaction) => sum + transaction.amount, + 0, + ); + + return total > 0 ? total : 0; + } + + return 0; +} + +export async function getGoalsByProfileId( + pool: Pool, + profileId: number, +): Promise { + const goals = await goalsQueries.getGoalsByProfileId(pool, profileId); + return goals; +} + +//enrich a goal with calculated progress +export async function enrichGoalWithProgress( + pool: Pool, + goal: Goal, + profileId: number, +): Promise { + const current_amount = await calculateCurrentAmount(pool, goal, profileId); + const progress_percentage = Math.min( + (current_amount / goal.target) * 100, + 100, + ); + + console.log( + "enriching goal with progress, cur amount:", + current_amount, + "progress percentage:", + progress_percentage, + " because target is:", + goal.target, + ); + + return { + ...goal, + current_amount, + progress_percentage, + }; +} + +export async function getUserGoals( + pool: Pool, + userId: number, +): Promise { + const profileId = await goalsQueries.getUserProfileId(pool, userId); + if (!profileId) return []; + + const goals = await goalsQueries.getGoalsByProfileId(pool, profileId); + + const goalsWithProgress = await Promise.all( + goals.map((goal) => enrichGoalWithProgress(pool, goal, profileId)), + ); + + return goalsWithProgress; +} + +//gets a single goal by id - this is used for patching goals +export async function getUserGoalById( + pool: Pool, + goalId: number, + userId: number, +): Promise { + const profileId = await goalsQueries.getUserProfileId(pool, userId); + if (!profileId) return null; + + const goal = await goalsQueries.getGoalById(pool, goalId, profileId); + if (!goal) return null; + + return enrichGoalWithProgress(pool, goal, profileId); +} + +export async function createUserGoal( + pool: Pool, + userId: number, + goalData: CreateGoalInput, +): Promise { + const profileId = await goalsQueries.getUserProfileId(pool, userId); + if (!profileId) { + throw new Error("User profile not found"); + } + + //validate goal count + const goalCount = await goalsQueries.getGoalCountByProfileId(pool, profileId); + if (goalCount >= MAX_GOALS_PER_PROFILE) { + throw new Error( + `Maximum ${MAX_GOALS_PER_PROFILE} goals allowed per profile`, + ); + } + + //validate input based on goal type + validateGoalInput(goalData); + + const newGoal = await goalsQueries.createGoal(pool, profileId, goalData); + if (!newGoal) { + throw new Error("Failed to create goal"); + } + + const enrichedGoal = await enrichGoalWithProgress(pool, newGoal, profileId); + if (!enrichedGoal) { + throw new Error("Failed to enrich goal with progress"); + } + + return enrichedGoal; +} + +export async function updateUserGoal( + pool: Pool, + goalId: number, + userId: number, + updates: UpdateGoalInput, +): Promise { + const profileId = await goalsQueries.getUserProfileId(pool, userId); + if (!profileId) { + throw new Error("User profile not found"); + } + + const existingGoal = await goalsQueries.getGoalById(pool, goalId, profileId); + if (!existingGoal) { + throw new Error("Goal not found"); + } + + //validate updates - maybe expand on this if needed + validateGoalUpdates(updates); + + const updated = await goalsQueries.updateGoal( + pool, + goalId, + profileId, + updates, + ); + if (!updated) { + throw new Error("Failed to update goal"); + } + + const updatedWithProgress = await enrichGoalWithProgress( + pool, + updated, + profileId, + ); + if (!updatedWithProgress) { + throw new Error("Failed to enrich goal with progress"); + } + + return updatedWithProgress; +} + +export async function deleteUserGoal( + pool: Pool, + goalId: number, + userId: number, +): Promise { + const profileId = await goalsQueries.getUserProfileId(pool, userId); + if (!profileId) { + throw new Error("User profile not found"); + } + + const deleted = await goalsQueries.deleteGoal(pool, goalId, profileId); + if (!deleted) { + throw new Error("Goal not found or already deleted"); + } + return true; +} + +//validation functions (same as before, but updated for new fields) +function validateGoalInput(input: CreateGoalInput): void { + if (!input.name || input.name.trim().length === 0) { + throw new Error("Goal name is required"); + } + + if (input.target <= 0) { + throw new Error("Target amount must be greater than 0"); + } + + if (input.type === "reduce_spending" && !input.category) { + throw new Error("Spending limit goals require a category"); + } +} + +function validateGoalUpdates(updates: UpdateGoalInput): void { + if (updates.target !== undefined && updates.target <= 0) { + throw new Error("Target amount must be greater than 0"); + } +} diff --git a/app/server/services/ts/user/src/queries/goals.ts b/app/server/services/ts/user/src/queries/goals.ts new file mode 100644 index 0000000..03f4c24 --- /dev/null +++ b/app/server/services/ts/user/src/queries/goals.ts @@ -0,0 +1,201 @@ +import { Pool, RowDataPacket, type ResultSetHeader } from "mysql2/promise"; +import type { Goal, CreateGoalInput, UpdateGoalInput } from "../types/Goals.ts"; + +interface GoalRow extends RowDataPacket, Goal {} + +//helper function to get all user profiles based on user account id +export async function getUserProfileId( + pool: Pool, + userId: number, +): Promise { + const [rows] = await pool.query( + `SELECT profile_id + FROM finus.finusAccount_profile + WHERE account_id = ?`, + [userId], + ); + + // console.log('Tried to find user profile id based on user id, found: ', rows); + return rows[0]?.profile_id || null; //getting the first profile for now, needs to be rewritten if multiple profiles are implemented (stretch) +} + +// Get goals for a specific profile +export async function getGoalsByProfileId( + pool: Pool, + profileId: number, +): Promise { + const [rows] = await pool.query( + `SELECT g.* + FROM finus.goal g + JOIN finus.profile_goal pg ON g.id = pg.goal_id + WHERE pg.profile_id = ? + ORDER BY g.id DESC`, + [profileId], + ); + return rows; +} + +//get a single goal by ID (with profile check) +export async function getGoalById( + pool: Pool, + goalId: number, + profileId: number, +): Promise { + const [rows] = await pool.query( + `SELECT g.* + FROM finus.goal g + JOIN finus.profile_goal pg ON g.id = pg.goal_id + WHERE g.id = ? AND pg.profile_id = ?`, + [goalId, profileId], + ); + return rows[0] || null; +} + +//gets a count of goals for a profile +export async function getGoalCountByProfileId( + pool: Pool, + userId: number, +): Promise { + const profileId = await getUserProfileId(pool, userId); + const [rows] = await pool.query( + `SELECT COUNT(*) as count + FROM finus.goal g + JOIN finus.profile_goal pg ON g.id = pg.goal_id + WHERE pg.profile_id = ?`, + [profileId], + ); + if (!rows[0]) return 0; + return rows[0].count; +} + +//creates a new goal and associate with profile +export async function createGoal( + pool: Pool, + profileId: number, + goalData: CreateGoalInput, +): Promise { + const connection = await pool.getConnection(); + const goalType = goalData.type; + let period = goalData.period; + if (goalType === "save") { + period = "m"; + } + try { + await connection.beginTransaction(); + const [result] = await connection.query( + `INSERT INTO finus.goal (name, type, category, target, period) + VALUES (?, ?, ?, ?, ?)`, + [ + goalData.name, + goalData.type, + goalData.category?.toLowerCase() || "unknown", + goalData.target, + period, + ], + ); + + const goalId = (result as ResultSetHeader).insertId; // result.insertId; + + //association with profile + await connection.query( + `INSERT INTO finus.profile_goal (profile_id, goal_id) + VALUES (?, ?)`, + [profileId, goalId], + ); + + await connection.commit(); + + //fetch and return the created goal for confirmation + const [newGoal] = await connection.query( + `SELECT * FROM finus.goal WHERE id = ?`, + [goalId], + ); + + return newGoal[0]; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} + +//update an existing goal - this is the patch path +export async function updateGoal( + pool: Pool, + goalId: number, + profileId: number, + updates: UpdateGoalInput, +): Promise { + //verify goal belongs to profile + const existing = await getGoalById(pool, goalId, profileId); + if (!existing) return undefined; + + //dynamic update query + const allowedFields = ["name", "category", "target", "period", "type"]; + const setClauses: string[] = []; + const values: unknown[] = []; + + //build update query dunamically since some fields may not be provided + for (const field of allowedFields) { + if (updates[field as keyof UpdateGoalInput] !== undefined) { + setClauses.push(`${field} = ?`); + values.push(updates[field as keyof UpdateGoalInput]); + } + } + + if (setClauses.length === 0) { + return existing; + } + + values.push(goalId); + + await pool.query( + `UPDATE finus.goal SET ${setClauses.join(", ")} WHERE id = ?`, + values, + ); + + //fetches updated goal + const [updated] = await pool.query( + `SELECT * FROM finus.goal WHERE id = ?`, + [goalId], + ); + + return updated[0]; +} + +//deletes a goal (remove from profile_goal association) +export async function deleteGoal( + pool: Pool, + goalId: number, + profileId: number, +): Promise { + const connection = await pool.getConnection(); + + try { + await connection.beginTransaction(); + + //remove association first + const [result] = await connection.query( + `DELETE FROM finus.profile_goal + WHERE goal_id = ? AND profile_id = ?`, + [goalId, profileId], + ); + + if ((result as ResultSetHeader).affectedRows === 0) { + await connection.rollback(); + return false; + } + + //delete the goal itself + await connection.query(`DELETE FROM finus.goal WHERE id = ?`, [goalId]); + + await connection.commit(); + return true; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} diff --git a/app/server/services/ts/user/src/queries/transactions.ts b/app/server/services/ts/user/src/queries/transactions.ts index 96dbb46..1ebbe58 100644 --- a/app/server/services/ts/user/src/queries/transactions.ts +++ b/app/server/services/ts/user/src/queries/transactions.ts @@ -20,3 +20,96 @@ export async function getAllTransactionsQuery( const [rows] = await pool.query(query, [userId]); return rows; } + +export async function getDateCategoryTransactionsQuery( + pool: Pool, + profileId: number, + category: string, + from: Date, + to: Date, +): Promise { + //convert dates to MySQL format + const fromStr = from.toISOString().slice(0, 19).replace("T", " "); + const toStr = to.toISOString().slice(0, 19).replace("T", " "); + + const query = ` + SELECT t.* + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + WHERE pfa.profile_id = ? + AND t.category = ? + AND t.date >= ? + AND t.date <= ? + ORDER BY t.date DESC + `; + + const [rows] = await pool.query(query, [ + profileId, + category, + fromStr, + toStr, + ]); + return rows; +} + +export async function getProfileCategoryTransactionsQuery( + pool: Pool, + profileId: number, + category: string, +): Promise { + const query = ` + SELECT t.* + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + WHERE pfa.profile_id = ? + AND t.category = ? + ORDER BY t.date DESC + `; + + const [rows] = await pool.query(query, [profileId, category]); + return rows; +} + +//returns all savings and expenses for a specific transaciton category +export async function getProfileCategorySavingsQuery( + pool: Pool, + profileId: number, + category: string, +): Promise { + const query = ` + SELECT t.* + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + WHERE pfa.profile_id = ? + AND t.category = ? + AND t.amount > 0 + ORDER BY t.date DESC + `; + + const [rows] = await pool.query(query, [profileId, category]); + return rows; +} + +//get only expenses (negative amounts) for a category +export async function getProfileCategoryExpensesQuery( + pool: Pool, + profileId: number, + category: string, +): Promise { + const query = ` + SELECT t.* + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + WHERE pfa.profile_id = ? + AND t.category = ? + AND t.amount < 0 + ORDER BY t.date DESC + `; + + const [rows] = await pool.query(query, [profileId, category]); + return rows; +} diff --git a/app/server/services/ts/user/src/types/Goals.ts b/app/server/services/ts/user/src/types/Goals.ts new file mode 100644 index 0000000..96c7508 --- /dev/null +++ b/app/server/services/ts/user/src/types/Goals.ts @@ -0,0 +1,34 @@ +export type GoalType = "save" | "reduce_spending"; +export type GoalPeriod = "m" | "w"; // | "na"; + +export interface Goal { + id: number; + name: string; + type: GoalType; + category?: string; + target: number; + period: GoalPeriod; + profile_id: number; //maybe add the current amount here too, it's just not stored in the db + // current_amount: number; +} + +export interface GoalWithProgress extends Goal { + current_amount: number; + progress_percentage: number; +} + +export interface CreateGoalInput { + name: string; + type: GoalType; + category?: string; + target: number; + period: GoalPeriod; +} + +export interface UpdateGoalInput { + name?: string; + category?: string; + target?: number; + period?: GoalPeriod; + type?: GoalType; +} diff --git a/app/server/services/ts/user/stryker.conf.json b/app/server/services/ts/user/stryker.conf.json new file mode 100644 index 0000000..725596c --- /dev/null +++ b/app/server/services/ts/user/stryker.conf.json @@ -0,0 +1,9 @@ +{ + "mutate": ["src/logic/goals.ts"], + "testRunner": "bun", + "plugins": ["stryker-mutator-bun-runner"], + "bun": { + "testFiles": ["./test/goals.test.ts"], + "timeout": 30000 + } +} diff --git a/app/server/services/ts/user/strykerNODE.conf.json b/app/server/services/ts/user/strykerNODE.conf.json new file mode 100644 index 0000000..fd0b1b6 --- /dev/null +++ b/app/server/services/ts/user/strykerNODE.conf.json @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "testRunner": "vitest", + "vitest": { + "configFile": "vitest.config.stryker.ts" + }, + "coverageAnalysis": "perTest", + "mutate": ["src/logic/goals.ts"], + "reporters": ["clear-text", "html"], + "concurrency": 4, + "timeoutMS": 10000 +} diff --git a/app/server/services/ts/user/test/dates.test.ts b/app/server/services/ts/user/test/dates.test.ts index 5acb76f..37c9593 100644 --- a/app/server/services/ts/user/test/dates.test.ts +++ b/app/server/services/ts/user/test/dates.test.ts @@ -1,4 +1,3 @@ -// tests/utils/dates.test.ts import { describe, expect, it } from "vitest"; import { generateDateRange } from "../src/utils/dates.ts"; import { diff --git a/app/server/services/ts/user/test/factories/goal.factory.ts b/app/server/services/ts/user/test/factories/goal.factory.ts new file mode 100644 index 0000000..f159404 --- /dev/null +++ b/app/server/services/ts/user/test/factories/goal.factory.ts @@ -0,0 +1,56 @@ +import type { Goal, CreateGoalInput } from "../../src/types/Goals.ts"; + +export function createMockGoal(overrides?: Partial): Goal { + return { + id: 1, + profile_id: 1, + name: "Save for Vacation", + type: "save", + category: "vacation", + target: 1000, + period: "na", + ...overrides, + }; +} + +export function createMockReduceSpendingGoal(overrides?: Partial): Goal { + return { + id: 2, + profile_id: 1, + name: "Reduce Grocery Spending", + type: "reduce_spending", + category: "groceries", + target: 400, + period: "m", + ...overrides, + }; +} + +export function createMockGoals(count: number = 3): Goal[] { + return Array.from({ length: count }, (_, i) => ({ + id: i + 1, + profile_id: 1, + name: `Goal ${i + 1}`, + type: i % 2 === 0 ? "save" : "reduce_spending", + category: i % 2 === 0 ? "vacation" : "groceries", + target: 500 + i * 100, + period: i % 2 === 0 ? "na" : "m", + })); +} + +export function createEmptyGoals(): Goal[] { + return []; +} + +export function createMockCreateGoalInput( + overrides?: Partial, +): CreateGoalInput { + return { + name: "Save for Vacation", + type: "save", + category: "travel", + target: 500, + period: "m", + ...overrides, + }; +} diff --git a/app/server/services/ts/user/test/goals.period.test.ts b/app/server/services/ts/user/test/goals.period.test.ts new file mode 100644 index 0000000..81e500f --- /dev/null +++ b/app/server/services/ts/user/test/goals.period.test.ts @@ -0,0 +1,64 @@ +import { vi, describe, expect, it, beforeEach, afterEach } from "vitest"; +import { enrichGoalWithProgress } from "../src/logic/goals.ts"; +import * as transactionsQueries from "../src/queries/transactions.ts"; +import type { Pool } from "mysql2/promise"; +import { createMockReduceSpendingGoal } from "./factories/goal.factory.ts"; + +vi.mock("../src/queries/transactions", () => ({ + getDateCategoryTransactionsQuery: vi.fn(), + getProfileCategoryTransactionsQuery: vi.fn(), +})); + +describe("Reduce Spending Goal Periods", () => { + const mockPool = {} as Pool; + const profileId = 456; + const mockGetDateCategoryTransactions = + transactionsQueries.getDateCategoryTransactionsQuery as vi.Mock; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("should calculate weekly spending correctly", async () => { + const mockGoal = createMockReduceSpendingGoal({ period: "w" }); + mockGetDateCategoryTransactions.mockResolvedValue([ + { amount: -50, category: "groceries" }, + { amount: -30, category: "groceries" }, + ]); + + const result = await enrichGoalWithProgress(mockPool, mockGoal, profileId); + + expect(result.current_amount).toBe(80); + + //verify the date range is correct (last 7 days from March 15) + const callArgs = mockGetDateCategoryTransactions.mock.calls[0]; + expect(callArgs[3]).toBeInstanceOf(Date); // startDate + expect(callArgs[4]).toBeInstanceOf(Date); // endDate + }); + + it("should calculate monthly spending correctly", async () => { + const mockGoal = createMockReduceSpendingGoal({ period: "m" }); + mockGetDateCategoryTransactions.mockResolvedValue([ + { amount: -100, category: "groceries" }, + { amount: -200, category: "groceries" }, + ]); + + const result = await enrichGoalWithProgress(mockPool, mockGoal, profileId); + + expect(result.current_amount).toBe(300); + }); + + it("should handle empty transactions for weekly period", async () => { + const mockGoal = createMockReduceSpendingGoal({ period: "w" }); + mockGetDateCategoryTransactions.mockResolvedValue([]); + + const result = await enrichGoalWithProgress(mockPool, mockGoal, profileId); + + expect(result.current_amount).toBe(0); + expect(result.progress_percentage).toBe(0); + }); +}); diff --git a/app/server/services/ts/user/test/goals.test.ts b/app/server/services/ts/user/test/goals.test.ts new file mode 100644 index 0000000..3e16aa0 --- /dev/null +++ b/app/server/services/ts/user/test/goals.test.ts @@ -0,0 +1,496 @@ +// tests/logic/goals.test.ts +import { vi, describe, expect, it, beforeEach } from "vitest"; +import { + getGoalsByProfileId, + getUserGoals, + getUserGoalById, + createUserGoal, + updateUserGoal, + deleteUserGoal, + enrichGoalWithProgress, +} from "../src/logic/goals.ts"; +import * as goalsQueries from "../src/queries/goals.ts"; +import * as transactionsQueries from "../src/queries/transactions.ts"; +// import * as goalsLogic from "../src/logic/goals.ts"; + +import type { Pool } from "mysql2/promise"; +import { + createMockGoal, + createMockReduceSpendingGoal, + // createMockGoalWithProgress, + createMockGoals, + createEmptyGoals, + createMockCreateGoalInput, +} from "./factories/goal.factory.ts"; +import type { Goal, GoalType } from "../src/types/Goals.ts"; + +vi.mock("../src/queries/goals", () => ({ + getUserProfileId: vi.fn(), + getGoalsByProfileId: vi.fn(), + getGoalById: vi.fn(), + getGoalCountByProfileId: vi.fn(), + createGoal: vi.fn(), + updateGoal: vi.fn(), + deleteGoal: vi.fn(), +})); + +vi.mock("../src/queries/transactions", () => ({ + getDateCategoryTransactionsQuery: vi.fn(), + getProfileCategoryTransactionsQuery: vi.fn(), +})); + +describe("Goals Logic", () => { + const mockPool = {} as Pool; + const userId = 123; + const profileId = 1; + const goalId = 1; + + const mockGetUserProfileId = goalsQueries.getUserProfileId as vi.Mock; + const mockGetGoalsByProfileId = goalsQueries.getGoalsByProfileId as vi.Mock; + const mockGetGoalById = goalsQueries.getGoalById as vi.Mock; + const mockGetGoalCountByProfileId = + goalsQueries.getGoalCountByProfileId as vi.Mock; + const mockCreateGoal = goalsQueries.createGoal as vi.Mock; + const mockUpdateGoal = goalsQueries.updateGoal as vi.Mock; + const mockDeleteGoal = goalsQueries.deleteGoal as vi.Mock; + const mockGetDateCategoryTransactions = + transactionsQueries.getDateCategoryTransactionsQuery as vi.Mock; + const mockGetProfileCategoryTransactions = + transactionsQueries.getProfileCategoryTransactionsQuery as vi.Mock; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe("getUserGoals", () => { + it("should return enriched goals for a user", async () => { + const mockGoals = createMockGoals(2); + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalsByProfileId.mockResolvedValue(mockGoals); + + //mock transactions for progress calculation + mockGetProfileCategoryTransactions.mockResolvedValue([ + { amount: 100, category: "vacation" }, + { amount: 150, category: "vacation" }, + ]); + mockGetDateCategoryTransactions.mockResolvedValue([]); + + const result = await getUserGoals(mockPool, userId); + + expect(result).toHaveLength(2); + expect(result[0].current_amount).toBeDefined(); + expect(result[0].progress_percentage).toBeDefined(); + expect(mockGetUserProfileId).toHaveBeenCalledWith(mockPool, userId); + expect(mockGetGoalsByProfileId).toHaveBeenCalledWith(mockPool, profileId); + }); + + it("should return empty array when no profile found", async () => { + mockGetUserProfileId.mockResolvedValue(null); + + const result = await getUserGoals(mockPool, userId); + + expect(result).toEqual([]); + expect(mockGetGoalsByProfileId).not.toHaveBeenCalled(); + }); + + it("should return empty array when no goals found", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalsByProfileId.mockResolvedValue(createEmptyGoals()); + + const result = await getUserGoals(mockPool, userId); + + expect(result).toEqual([]); + }); + }); + + describe("getUserGoalById", () => { + it("should return enriched goal by id", async () => { + const mockGoal = createMockGoal(); + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalById.mockResolvedValue(mockGoal); + mockGetProfileCategoryTransactions.mockResolvedValue([ + { amount: 100, category: "vacation" }, + ]); + + const result = await getUserGoalById(mockPool, goalId, userId); + + expect(result).toBeDefined(); + expect(result?.id).toBe(goalId); + expect(result?.current_amount).toBeDefined(); + expect(mockGetUserProfileId).toHaveBeenCalledWith(mockPool, userId); + expect(mockGetGoalById).toHaveBeenCalledWith(mockPool, goalId, profileId); + }); + + it("should return null when no profile found", async () => { + mockGetUserProfileId.mockResolvedValue(null); + + const result = await getUserGoalById(mockPool, goalId, userId); + + expect(result).toBeNull(); + }); + + it("should return null when goal not found", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalById.mockResolvedValue(null); + + const result = await getUserGoalById(mockPool, goalId, userId); + + expect(result).toBeNull(); + }); + }); + + describe("createUserGoal", () => { + const createInput = createMockCreateGoalInput(); + + const createdGoal = { + ...createInput, + id: goalId, + current_amount: 0, + progress_percentage: 0, + }; + + it("should create a new goal successfully", async () => { + // const mockNewGoal = createMockGoal(); + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalCountByProfileId.mockResolvedValue(2); + mockCreateGoal.mockResolvedValue(createdGoal); + mockGetProfileCategoryTransactions.mockResolvedValue([]); + + const result = await createUserGoal(mockPool, userId, createInput); + + expect(result).toBeDefined(); + expect(result.name).toBe(createInput.name); + expect(mockCreateGoal).toHaveBeenCalledWith( + mockPool, + profileId, + createInput, + ); + expect(mockGetGoalCountByProfileId).toHaveBeenCalledWith( + mockPool, + profileId, + ); + }); + + it("should throw error when profile not found", async () => { + mockGetUserProfileId.mockResolvedValue(null); + + await expect( + createUserGoal(mockPool, userId, createInput), + ).rejects.toThrow("User profile not found"); + }); + + it("should throw error when max goals reached", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalCountByProfileId.mockResolvedValue(5); + + await expect( + createUserGoal(mockPool, userId, createInput), + ).rejects.toThrow("Maximum 5 goals allowed per profile"); + }); + + it("should throw error when goal name is empty", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalCountByProfileId.mockResolvedValue(2); + const invalidInput = { ...createInput, name: " " }; + + await expect( + createUserGoal(mockPool, userId, invalidInput), + ).rejects.toThrow("Goal name is required"); + }); + + it("should throw error when target is zero or negative", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalCountByProfileId.mockResolvedValue(2); + const invalidInput = { ...createInput, target: 0 }; + + await expect( + createUserGoal(mockPool, userId, invalidInput), + ).rejects.toThrow("Target amount must be greater than 0"); + }); + + it("should throw error when reduce_spending goal has no category", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalCountByProfileId.mockResolvedValue(2); + const invalidInput = { + ...createInput, + type: "reduce_spending" as GoalType, + category: "", + }; + + await expect( + createUserGoal(mockPool, userId, invalidInput), + ).rejects.toThrow("Spending limit goals require a category"); + }); + }); + + describe("updateUserGoal", () => { + const updates = { name: "Updated Goal", target: 2000 }; + + it("should update a goal successfully", async () => { + const existingGoal = createMockGoal(); + const updatedGoal = { ...existingGoal, ...updates }; + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalById.mockResolvedValue(existingGoal); + mockUpdateGoal.mockResolvedValue(updatedGoal); + mockGetProfileCategoryTransactions.mockResolvedValue([]); + + const result = await updateUserGoal(mockPool, goalId, userId, updates); + + expect(result).toBeDefined(); + expect(result.name).toBe(updates.name); + expect(result.target).toBe(updates.target); + expect(mockUpdateGoal).toHaveBeenCalledWith( + mockPool, + goalId, + profileId, + updates, + ); + }); + + it("should throw error when profile not found", async () => { + mockGetUserProfileId.mockResolvedValue(null); + + await expect( + updateUserGoal(mockPool, goalId, userId, updates), + ).rejects.toThrow("User profile not found"); + }); + + it("should throw error when goal not found", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalById.mockResolvedValue(null); + + await expect( + updateUserGoal(mockPool, goalId, userId, updates), + ).rejects.toThrow("Goal not found"); + }); + + it("should throw error when target is zero or negative", async () => { + const existingGoal = createMockGoal(); + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalById.mockResolvedValue(existingGoal); + const invalidUpdates = { target: 0 }; + + await expect( + updateUserGoal(mockPool, goalId, userId, invalidUpdates), + ).rejects.toThrow("Target amount must be greater than 0"); + }); + + it("should throw error when updateGoal returns null", async () => { + const existingGoal = createMockGoal(); + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalById.mockResolvedValue(existingGoal); + mockUpdateGoal.mockResolvedValue(null); // Simulate failed update + + await expect( + updateUserGoal(mockPool, goalId, userId, updates), + ).rejects.toThrow("Failed to update goal"); + }); + }); + + describe("deleteUserGoal", () => { + it("should delete a goal successfully", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockDeleteGoal.mockResolvedValue(true); + + const result = await deleteUserGoal(mockPool, goalId, userId); + + expect(result).toBe(true); + expect(mockDeleteGoal).toHaveBeenCalledWith(mockPool, goalId, profileId); + }); + + it("should throw error when profile not found", async () => { + mockGetUserProfileId.mockResolvedValue(null); + + await expect(deleteUserGoal(mockPool, goalId, userId)).rejects.toThrow( + "User profile not found", + ); + }); + + it("should throw error when goal not found", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockDeleteGoal.mockResolvedValue(false); + + await expect(deleteUserGoal(mockPool, goalId, userId)).rejects.toThrow( + "Goal not found or already deleted", + ); + }); + }); + + describe("enrichGoalWithProgress", () => { + it("should enrich a save goal with progress from savings transactions", async () => { + const mockGoal = createMockGoal(); + mockGetProfileCategoryTransactions.mockResolvedValue([ + { amount: 100, category: "vacation" }, + { amount: 150, category: "vacation" }, + ]); + + const result = await enrichGoalWithProgress( + mockPool, + mockGoal, + profileId, + ); + + expect(result.current_amount).toBe(250); + expect(result.progress_percentage).toBe(25); // 250/1000 * 100 + expect(mockGetProfileCategoryTransactions).toHaveBeenCalledWith( + mockPool, + profileId, + mockGoal.category, + ); + }); + + it("should enrich a reduce_spending goal with progress from expenses", async () => { + const mockGoal = createMockReduceSpendingGoal(); + mockGetDateCategoryTransactions.mockResolvedValue([ + { amount: -100, category: "groceries" }, + { amount: -150, category: "groceries" }, + ]); + + const result = await enrichGoalWithProgress( + mockPool, + mockGoal, + profileId, + ); + + expect(result.current_amount).toBe(250); + expect(result.progress_percentage).toBe(62.5); // 250/400 * 100 + expect(mockGetDateCategoryTransactions).toHaveBeenCalledWith( + mockPool, + profileId, + mockGoal.category, + expect.any(Date), + expect.any(Date), + ); + }); + + it("should cap progress at 100%", async () => { + const mockGoal = createMockGoal({ target: 200 }); + mockGetProfileCategoryTransactions.mockResolvedValue([ + { amount: 300, category: "vacation" }, + ]); + + const result = await enrichGoalWithProgress( + mockPool, + mockGoal, + profileId, + ); + + expect(result.current_amount).toBe(300); + expect(result.progress_percentage).toBe(100); // capped at 100 + }); + + it("should return 0 current amount when no transactions", async () => { + const mockGoal = createMockGoal(); + mockGetProfileCategoryTransactions.mockResolvedValue([]); + + const result = await enrichGoalWithProgress( + mockPool, + mockGoal, + profileId, + ); + + expect(result.current_amount).toBe(0); + expect(result.progress_percentage).toBe(0); + }); + + it("should handle negative totals for save goals (more expenses than savings)", async () => { + const mockGoal = createMockGoal(); + mockGetProfileCategoryTransactions.mockResolvedValue([ + { amount: 100, category: "vacation" }, + { amount: -300, category: "vacation" }, // negative transaction (expense) + ]); + + const result = await enrichGoalWithProgress( + mockPool, + mockGoal, + profileId, + ); + + expect(result.current_amount).toBe(0); // total is -200, should return 0 + }); + }); + + describe("getGoalsByProfileId", () => { + it("should return goals for a profile", async () => { + const mockGoals = createMockGoals(3); + mockGetGoalsByProfileId.mockResolvedValue(mockGoals); + + const result = await getGoalsByProfileId(mockPool, profileId); + + expect(result).toHaveLength(3); + expect(mockGetGoalsByProfileId).toHaveBeenCalledWith(mockPool, profileId); + }); + + it("should return empty array when no goals", async () => { + mockGetGoalsByProfileId.mockResolvedValue([]); + + const result = await getGoalsByProfileId(mockPool, profileId); + + expect(result).toEqual([]); + }); + }); + + describe("calculateCurrentAmount - unknown goal type via enrichGoalWithProgress", () => { + it("should return 0 for unknown goal type", async () => { + const invalidGoal = { + id: 999, + profile_id: 1, + name: "Invalid Goal", + type: "invalid_type" as GoalType, + category: "test", + target: 1000, + period: "m", + } as Goal; + + mockGetUserProfileId.mockResolvedValue(profileId); + + mockGetProfileCategoryTransactions.mockResolvedValue([]); + mockGetDateCategoryTransactions.mockResolvedValue([]); + + const result = await enrichGoalWithProgress( + mockPool, + invalidGoal, + profileId, + ); + expect(result.current_amount).toBe(0); + }); + + it("should handle profileId null gracefully", async () => { + const validGoal = createMockGoal(); + mockGetUserProfileId.mockResolvedValue(null); + const result = await enrichGoalWithProgress(mockPool, validGoal, null); + + expect(result.current_amount).toBe(0); + }); + }); + + // it("should throw error when enrichGoalWithProgress returns null", async () => { + // //this fails on docker for some reason, but runs fine locally, unknown if it works in GitHub Actions so it's untouched + // const mockNewGoal = createMockGoal(); + // mockGetUserProfileId.mockResolvedValue(profileId); + // mockGetGoalCountByProfileId.mockResolvedValue(2); + // mockCreateGoal.mockResolvedValue(mockNewGoal); + // const createInput = createMockCreateGoalInput(); + // //spy on enrichGoalWithProgress and make it return null + // const enrichSpy = vi + // .spyOn(goalsLogic, "enrichGoalWithProgress") + // .mockResolvedValue(null); + + // await expect(createUserGoal(mockPool, userId, createInput)).rejects.toThrow( + // "Failed to enrich goal with progress", + // ); + + // enrichSpy.mockRestore(); + // }); + + it("should throw error when createGoal returns undefined", async () => { + mockGetUserProfileId.mockResolvedValue(profileId); + mockGetGoalCountByProfileId.mockResolvedValue(2); + mockCreateGoal.mockResolvedValue(undefined); //simulate undefined result + const createInput = createMockCreateGoalInput(); + + await expect(createUserGoal(mockPool, userId, createInput)).rejects.toThrow( + "Failed to create goal", + ); + }); +}); diff --git a/app/server/services/ts/user/vitest.config.stryker.ts b/app/server/services/ts/user/vitest.config.stryker.ts new file mode 100644 index 0000000..8ac7718 --- /dev/null +++ b/app/server/services/ts/user/vitest.config.stryker.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["test/**/*.test.ts"], + coverage: { + provider: "v8", + reporter: ["json", "text"], + }, + }, +}); diff --git a/documentation/Finus_Test_Plan.md b/documentation/Finus_Test_Plan.md index bff544c..cc74133 100644 --- a/documentation/Finus_Test_Plan.md +++ b/documentation/Finus_Test_Plan.md @@ -1,32 +1,32 @@ - ***Note that you can refine your testing plan as the project development goes. Keep the change log as follow:*** *Change-log* -| Version | Change Date | By | Description | -| :---: | :---: | :---: | :---: | -| version number | Date of Change | Name of person who made changes | Description of the changes made | -| 1.0 | March 2 | Roman Tebel | Initial write up | -| 1.1 | March 5 | Deep Patel | Added more details to the scope, roles, and testing | +| Version | Change Date | By | Description | +| :------------: | :------------: | :-----------------------------: | :-------------------------------------------------: | +| version number | Date of Change | Name of person who made changes | Description of the changes made | +| 1.0 | March 2 | Roman Tebel | Initial write up | +| 1.1 | March 5 | Deep Patel | Added more details to the scope, roles, and testing | +| 1.2 | March 27 | Roman | Updating testing procedures | 1. # **Introduction** - 1. ## **Scope** + 1. ## **Scope** These are the features we have implemented for Sprint 2 and have created tests for: 1\. User Authentication -1. Sign up -2. Login +1. Sign up +2. Login 3. Logout 2\. Visualization Dashboard -1. Savings chart logic -2. Income flow chart logic -3. Budget calculation and performance against expenses -4. Lookup of transactions +1. Savings chart logic +2. Income flow chart logic +3. Budget calculation and performance against expenses +4. Lookup of transactions 5. Aggregation of snapshot data 3\. User Data Input @@ -35,88 +35,84 @@ These are the features we have implemented for Sprint 2 and have created tests f 2. Data input 3. Validating forms and files - The following are the features to be implemented in future sprints: Financial Goals, Financial Projections, Stock and FOREX Tracking, Collaborative Budgets (stretch), and ML Integration (stretch). The scope includes functional verification through unit, integration, acceptance, and regression testing for the features implemented. The unit and integration tests will confirm code quality and can potentially evaluate some of the project requirements. A major portion of project requirements in our case must be validated through a manual walkthrough within the client. -Load testing will be conducted in Sprint 3 in order to ensure that the server can support the expected usage load, specified in the course outline for the project. +Load testing will be conducted in Sprint 4 in order to ensure that the server can support the expected usage load, specified in the course outline for the project. Mutation testing will be conducted in Sprint 3 in order to expand code coverage and make the project more robust. - - -2. ## **Roles and Responsibilities** +2. ## **Roles and Responsibilities** Each team member is assigned one or more of the following roles: -1. Frontend Developer -2. Backend Developer -3. DevOps -4. Tester +1. Frontend Developer +2. Backend Developer +3. DevOps +4. Tester 5. Quality Assurance -| Name | Net ID | GitHub username | Role | -| :---- | :---- | :---- | :---- | -| Roman Tebel | 7929015 | TebelR | Frontend Developer \+ Backend Developer \+ Tester | -| Logan Decock | 7966258 | MeasureOneCodeTwice | Frontend Developer \+ Backend Developer \+ DevOps | -| Jackie Mei | 7882240 | JackieMei3 | Frontend Developer \+ Backend Developer \+ QA | -| Dylan Prabagaran | 7898220 | dyll87 | Frontend Developer \+ Backend Developer \+ Tester | -| Deep Patel | 7957389 | deep-n-patel | Frontend Developer \+ Backend Developer \+ Tester | -| Hoang Huy Truong | 7960938 | HuyTruong24 | Frontend Developer \+ Backend Developer \+ Tester | +| Name | Net ID | GitHub username | Role | +| :--------------- | :------ | :------------------ | :----------------------------------------------- | +| Roman Tebel | 7929015 | TebelR | Frontend Developer\+ Backend Developer \+ Tester | +| Logan Decock | 7966258 | MeasureOneCodeTwice | Frontend Developer\+ Backend Developer \+ DevOps | +| Jackie Mei | 7882240 | JackieMei3 | Frontend Developer\+ Backend Developer \+ QA | +| Dylan Prabagaran | 7898220 | dyll87 | Frontend Developer\+ Backend Developer \+ Tester | +| Deep Patel | 7957389 | deep-n-patel | Frontend Developer\+ Backend Developer \+ Tester | +| Hoang Huy Truong | 7960938 | HuyTruong24 | Frontend Developer\+ Backend Developer \+ Tester | **Role Details** -* Frontend Developer - * Build and maintain the user interface of the application. - * Connect the frontend to backend services through APIs. - * Ensure the application is responsive and user-friendly. - * Test and debug frontend code to maintain performance and quality. - -* Backend Developer - * Develop and maintain server-side logic and APIs. - * Manage databases and data processing. - * Implement security, authentication, and business logic. - * Ensure the system is reliable, scalable, and well-tested. - -* DevOps - * Manager docker compose files, environments and secrets - * Create and maintain CI/CD pipelines - * Environment set up such linting and formatting as a pre-commit hooks - -* Tester - * Creates testing plans and strategies - * Responsible for performing both manual and automated testing - * Makes sure that the software meets the performance and quality standards - -* Quality Assurance - * Searches/Finds bugs in development & build process as well as in production. - +* Frontend Developer + + * Build and maintain the user interface of the application. + * Connect the frontend to backend services through APIs. + * Ensure the application is responsive and user-friendly. + * Test and debug frontend code to maintain performance and quality. +* Backend Developer + + * Develop and maintain server-side logic and APIs. + * Manage databases and data processing. + * Implement security, authentication, and business logic. + * Ensure the system is reliable, scalable, and well-tested. +* DevOps + + * Manager docker compose files, environments and secrets + * Create and maintain CI/CD pipelines + * Environment set up such linting and formatting as a pre-commit hooks +* Tester + + * Creates testing plans and strategies + * Responsible for performing both manual and automated testing + * Makes sure that the software meets the performance and quality standards +* Quality Assurance + + * Searches/Finds bugs in development & build process as well as in production. 2. # **Test Methodology** - 1. ## **Automated regression testing** + 1. ## **Automated regression testing** **2.1.1 Test Levels** -Test levels define the Types of Testing to be executed on the Application Under Test (AUT). In this course, unit testing, integration testing, acceptance testing, regression testing, and load testing are mandatory. +Test levels define the Types of Testing to be executed on the Application Under Test (AUT). In this course, unit testing, integration testing, acceptance testing, regression testing, and load testing are mandatory. **Requirements**: -- For unit testing, at least 10 unit tests for EACH core feature to cover the code related to each core feature. -- For integration testing, at least 10 in total to cover core features. -- Acceptance testing for each core feature. Let’s use end-user test for this. You can ask real end-user or your team members to go through each user story and see if the requirements are meet. -- For regression testing, need to execute all above unit tests \+ integration tests you have for each commit pushed to main branch. - +- For unit testing, at least 10 unit tests for EACH core feature to cover the code related to each core feature. +- For integration testing, at least 10 in total to cover core features. +- Acceptance testing for each core feature. Let’s use end-user test for this. You can ask real end-user or your team members to go through each user story and see if the requirements are meet. +- For regression testing, need to execute all above unit tests \+ integration tests you have for each commit pushed to main branch. -| Test Level | Scope & Requirement | Methodology (How will you do this?) | -| :---- | :---- | :---- | -| **Unit Testing** | **User Authentication:** 20 tests
**User Input:** 30 tests
**Visualization Dashboard:** 50 in Python, 46 in TypeScript
**Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | -| **Integration Testing** | **10 tests total** covering interactions between features. | *Running tests in their environment with microservices fully operational and communicating with each other.* | -| **Acceptance Testing** | **End-user testing** for every user story. | *Team members/external users will perform Manual Walkthroughs based on User Story criteria. Deficiencies and potential improvements will be documented.* | -| **Regression Testing** | Unit \+ Integration tests executed on **every push to main branch**. | *We have configured a GitHub Actions CI pipeline to run all tests automatically.* | +| Test Level | Scope & Requirement | Methodology (How will you do this?) | +| :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Unit Testing** | **User Authentication:** 20 tests `
` **User Input:** 30 tests `
` **Visualization Dashboard:** 50 in Python, 46 in TypeScript `
` **Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | +| **Integration Testing** | **10 tests total** covering interactions between features. | *Running tests in their environment with microservices fully operational and communicating with each other. These tests have to be executed locally as CI/CD does not have enough flexibility for this.* | +| **Acceptance Testing** | **End-user testing** for every user story. | *Team members/external users will perform Manual Walkthroughs based on User Story criteria. Deficiencies and potential improvements will be documented.* | +| **Regression Testing** | Unit\+ Integration tests executed on **every push to main branch**. | *We have configured a GitHub Actions CI pipeline to run all tests automatically.* | **2.1.2 CI/CD Regression Workflows** @@ -126,19 +122,22 @@ When a commit is pushed to a feature branch, github actions will also build and 2. ## **Mutation Testing (Test effectiveness)** -*Skip for Sprint 2\.* +*We used mutmut for mutation testing in our python modules: analytics and market. Separate docker files exist for mutation testing if you want to try running things locally.* + +Stryker is the main supported mutation testing tool for JS and TS, but it is not supported in bun. There is a community-made plugin to enable stryker for bun, but it is in its earliest stages of development. Stryker executes, but can't gather coverage from tests in bun, so it appears as if no mutants are getting killed no matter what is done in unit tests. + +We attempted to run stryker with vitest in node, but this results in a lot of library conflicts and version mismatches as bun and node use the same exact configuration and organization files for packages. 3. ## **Load Testing** -*Skip for Sprint 2\.* +*Skip for Sprint 3\.* -3. # **Terms/Acronyms** +3. # **Terms/Acronyms** Make a mention of any terms or acronyms used in the project -| TERM/ACRONYM | DEFINITION | -| :---- | :---- | -| API | Application Program Interface | -| AUT | Application Under Test | -| JWT | JSON Web Token | - +| TERM/ACRONYM | DEFINITION | +| :----------- | :---------------------------- | +| API | Application Program Interface | +| AUT | Application Under Test | +| JWT | JSON Web Token | diff --git a/documentation/acceptance tests/goals.md b/documentation/acceptance tests/goals.md new file mode 100644 index 0000000..e5a1a66 --- /dev/null +++ b/documentation/acceptance tests/goals.md @@ -0,0 +1,21 @@ +Acceptance criteria from user story: + +1. User budget is aligned with the selected financial goal + +2. User budget recommends changes to the user’s financial habits, should the user stray away from their financial goal + +3. Users can set goals with an amount, data, and goal type ("reduce spending") + + +The above criteria can be tested through the client by following the steps below. + +1. Log in with an email j@j.com and password pwd +2. Observe that there is a generated budget. Pick a random category that has a budget generated for it at the weekly period. +3. Locate a checkmark in the top left corner of the page and hover your mouse over it +4. Observe the goals panel appearing to the right of the checkmark +5. Assuming that this is a pre-populated account, there may be strangely-named goals. Click the "+ Add Goal" button +6. Observe a new goal appearing in the panel - this meets the 3rd acceptance criteria +7. Click on the goal to expand it in more detail and make it editable +8. Can change any field in the goal, but for the scenario, set the goal type to 'Save Money' and category to the random category that you picked in step 2. Apply the edit +9. Refresh the page or change the period of the budget and go back to monthly, and observe that the budget is readjusted. If your category was considered a necessity, it's budget is likely increased, if it is a want, it is likely lowered. More of your average income is allocated to savings at a flat 5% rate. This meets the 1st acceptance criteria +10. Change the created goal's type to "Reduce Spending", and refresh the budget after applying edits. Depending on how close your expenses are to the target for the selected period, observe that the budget is adjusted with varying levels of punishment for wants. The category "entertainment", is classified as a want internally, as an example. Overall, the budget for needs is also slightly reduced. This meets the 2nd acceptance criteria as needed. diff --git a/documentation/sequence-diagrams/goals/Goals Sequence Diagram.png b/documentation/sequence-diagrams/goals/Goals Sequence Diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..d653ca9b15317d0fa4885ab2407577a39f053474 GIT binary patch literal 101935 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%0ApKFxsRs*Q1~{{PXLDv7RWi@jBgY8G_{Eh^^ju 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 Date: Fri, 27 Mar 2026 13:09:42 -0500 Subject: [PATCH 189/204] Squashing feature user projection - merge into dev (#90) * Server: implemented debt and saving endpoints * More * Server: commented out new endpoints in auth service * Server: implemented debt payoff calculation but it's not working properly * Adjusted to using connection pool from db.ts to be used for sql queries and move some functions to their own file. * Created page for projection * Setup the graph for projection page and API calls for debt * Server: Fixed endpoints for fetching and creating user debts * Added handling of when there's is not data for the graph * Server: Fixed endpoints for fetching and creating saving account * Server: Finished predicted debt payoff calculation * Server: Implemented compound interest logic * Server: Implemented fetching historical transactions of saving financial account in user service * Setup the rest of the projected page, finished the api calls and fix some bugs * Server: Implemented unit tests for saving queries * Server: More unit tests * Projection graph for debt is workings * Typo fix * Server: finished compound interest computing and did some cleanups * Made so the graph can handle mutliples lines and handles getting saving projection response * Server: modified compound interest logic * Server: Added more unit tests * Saving projection graph works, also adjusted how forms look * Server: added user flow diagram * Fixed issues with quries - previous queries treated userID as profileID which steps over the user to profile to financial account hierarchy in schema * bug fix * bug fix * bug fix - reverted schema to old vairant for fin account type * temoporarily disabling integration tests due to a db error * disablign integration tests that rely on db * bug fix --------- Co-authored-by: Huy Truong Co-authored-by: Jackie Mei Co-authored-by: TebelR --- .github/workflows/run_tests.yml | 3 +- .vscode/settings.json | 3 + app/client/Dockerfile | 14 + app/client/src/App.tsx | 13 + app/client/src/api/Account.ts | 10 +- app/client/src/api/Debt.ts | 59 ++ app/client/src/api/Saving.ts | 59 ++ app/client/src/api/Transaction.ts | 4 +- app/client/src/components/AccountForm.tsx | 68 ++- app/client/src/components/AccountListCard.tsx | 2 +- app/client/src/components/NavBar.tsx | 2 + app/client/src/components/ProjectionGraph.tsx | 90 +++ app/client/src/components/SelectAccount.tsx | 35 ++ app/client/src/components/SelectDebt.tsx | 34 ++ app/client/src/components/TransactionCard.tsx | 4 +- app/client/src/components/TransactionList.tsx | 24 +- app/client/src/components/TransationForm.tsx | 40 +- app/client/src/components/userForm.css | 36 +- app/client/src/enum/AccountCategory.ts | 5 +- app/client/src/pages/ProjectionPage.tsx | 536 ++++++++++++++++++ app/client/src/types/Debt.ts | 16 + app/client/src/types/requestTypes.ts | 17 + app/client/src/types/responseTypes.ts | 33 ++ .../src/utils/ValidateProjectionRequest.ts | 35 ++ app/server/docker-compose.test.yml | 26 +- app/server/services/database/schema.sql | 4 +- .../python/analytics/src/logic/debt.py | 48 ++ .../python/analytics/src/logic/savings.py | 159 +++++- .../services/python/analytics/src/main.py | 33 +- .../python/analytics/src/models/schemas.py | 51 +- .../python/analytics/src/queries/savings.py | 1 - .../python/analytics/test/conftest.py | 55 +- .../python/analytics/test/test_debts.py | 52 ++ .../python/analytics/test/test_savings.py | 41 +- .../services/ts/api-gateway/src/index.ts | 36 ++ .../services/ts/user/integration/setup.ts | 6 + app/server/services/ts/user/src/CheckUser.ts | 39 ++ .../services/ts/user/src/DataConversion.ts | 5 + app/server/services/ts/user/src/handleJWT.ts | 7 +- app/server/services/ts/user/src/index.ts | 32 +- app/server/services/ts/user/src/logic/debt.ts | 18 + .../services/ts/user/src/logic/saving.ts | 41 ++ .../services/ts/user/src/queries/debt.ts | 74 +++ .../services/ts/user/src/queries/saving.ts | 77 +++ .../ts/user/src/queries/transactions.ts | 14 + .../services/ts/user/src/routes/account.ts | 142 +++-- .../services/ts/user/src/routes/debt.ts | 44 ++ .../services/ts/user/src/routes/saving.ts | 76 +++ .../ts/user/src/routes/transaction.ts | 98 ++-- .../ts/user/src/types/BadRequestError.ts | 7 + .../ts/user/src/types/DebtInfoResponse.ts | 9 + .../ts/user/src/types/ExpectedDebtDueDate.ts | 8 + .../src/types/ExpectedDebtDueDateResponse.ts | 7 + .../user/src/types/FinancialAccountRequest.ts | 20 + .../ts/user/src/types/FinancialAccountType.ts | 7 + .../ts/user/src/types/InvestmentType.ts | 4 + .../ts/user/src/types/SavingAccountType.ts | 7 + .../ts/user/src/types/SavingInfoRequest.ts | 8 + .../ts/user/src/types/SavingInfoResponse.ts | 11 + .../ts/user/src/types/TransactionDto.ts | 3 + .../ts/user/src/types/UnauthorizedAccess.ts | 8 + .../services/ts/user/test/logic_debts.test.ts | 212 +++++++ .../ts/user/test/logic_savings.test.ts | 121 ++++ .../ts/user/test/logic_transactions.test.ts | 2 + .../ts/user/test/queries_savings.test.ts | 211 +++++++ .../user-projection/user-flow-diagram.png | Bin 0 -> 72305 bytes 66 files changed, 2761 insertions(+), 205 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 app/client/src/api/Debt.ts create mode 100644 app/client/src/api/Saving.ts create mode 100644 app/client/src/components/ProjectionGraph.tsx create mode 100644 app/client/src/components/SelectAccount.tsx create mode 100644 app/client/src/components/SelectDebt.tsx create mode 100644 app/client/src/pages/ProjectionPage.tsx create mode 100644 app/client/src/types/Debt.ts create mode 100644 app/client/src/types/requestTypes.ts create mode 100644 app/client/src/utils/ValidateProjectionRequest.ts create mode 100644 app/server/services/python/analytics/src/logic/debt.py create mode 100644 app/server/services/python/analytics/test/test_debts.py create mode 100644 app/server/services/ts/user/src/CheckUser.ts create mode 100644 app/server/services/ts/user/src/DataConversion.ts create mode 100644 app/server/services/ts/user/src/logic/debt.ts create mode 100644 app/server/services/ts/user/src/logic/saving.ts create mode 100644 app/server/services/ts/user/src/queries/debt.ts create mode 100644 app/server/services/ts/user/src/queries/saving.ts create mode 100644 app/server/services/ts/user/src/routes/debt.ts create mode 100644 app/server/services/ts/user/src/routes/saving.ts create mode 100644 app/server/services/ts/user/src/types/BadRequestError.ts create mode 100644 app/server/services/ts/user/src/types/DebtInfoResponse.ts create mode 100644 app/server/services/ts/user/src/types/ExpectedDebtDueDate.ts create mode 100644 app/server/services/ts/user/src/types/ExpectedDebtDueDateResponse.ts create mode 100644 app/server/services/ts/user/src/types/FinancialAccountRequest.ts create mode 100644 app/server/services/ts/user/src/types/FinancialAccountType.ts create mode 100644 app/server/services/ts/user/src/types/InvestmentType.ts create mode 100644 app/server/services/ts/user/src/types/SavingAccountType.ts create mode 100644 app/server/services/ts/user/src/types/SavingInfoRequest.ts create mode 100644 app/server/services/ts/user/src/types/SavingInfoResponse.ts create mode 100644 app/server/services/ts/user/src/types/TransactionDto.ts create mode 100644 app/server/services/ts/user/src/types/UnauthorizedAccess.ts create mode 100644 app/server/services/ts/user/test/logic_debts.test.ts create mode 100644 app/server/services/ts/user/test/logic_savings.test.ts create mode 100644 app/server/services/ts/user/test/queries_savings.test.ts create mode 100644 documentation/sequence-diagrams/user-projection/user-flow-diagram.png diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 192b8a0..1a6d321 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -32,8 +32,7 @@ jobs: analytics-test \ market-test \ market-mutation-test \ - market-integration-test \ - user-integration-test + market-integration-test do echo "::group::Running $service" if ! docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm "$service"; then diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..acf6d3b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system" +} diff --git a/app/client/Dockerfile b/app/client/Dockerfile index ec7fcdd..8f0fe98 100644 --- a/app/client/Dockerfile +++ b/app/client/Dockerfile @@ -1,3 +1,17 @@ +FROM oven/bun:1.3.9-slim AS builder + +WORKDIR /app + +COPY package.json bun.lockb ./ +RUN --mount=type=cache,target=/root/.bun/install/cache \ + bun install + +COPY tsconfig.* . +COPY vite.config.ts . +COPY index.* . +COPY public ./public +COPY src ./src + FROM finus-webserver-base:local as base RUN bun run build diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 32fb97a..391861b 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -12,6 +12,7 @@ import type { AuthSession, AuthUser, AuthApiResponse } from "./types/authTypes"; import DashboardPage from "./pages/DashboardPage.tsx"; import MarketsPage from "./pages/MarketsPage"; import AppLayout from "./components/AppLayout.tsx"; +import ProjectionPage from "./pages/ProjectionPage.tsx"; import { syncPinnedMarketsResetKey } from "./utils/marketStorage"; //import { loadSession, saveSession, clearSession } from "./utils/storage.ts"; //import { requestAuth } from "./api/AuthAPI"; @@ -234,6 +235,8 @@ function App() { ) } /> + + {session && ( }> )} + + {session && ( + }> + } + /> + + )} + } /> {/**Code below is only used for dashboard development purposes */} diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index 9177437..e1deda3 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -7,10 +7,18 @@ const requestUrl = "http://localhost:3000/api/accounts"; //Sends a request to get different accounts the user has export async function getUserAccounts( session: AuthSession, + type?: string, ): Promise { + let url = requestUrl; + + //Determine if we're targetting a specific type + if (type) { + url = `${requestUrl}/?type=${type}`; + } + try { //Sends a http request and waits for a response - const response = await fetch(requestUrl, { + const response = await fetch(url, { method: "GET", headers: { Authorization: `Bearer ${session.token}` }, }); diff --git a/app/client/src/api/Debt.ts b/app/client/src/api/Debt.ts new file mode 100644 index 0000000..6b32969 --- /dev/null +++ b/app/client/src/api/Debt.ts @@ -0,0 +1,59 @@ +import type { DebtPayoffResponse } from "../types/responseTypes"; +import type { AuthSession } from "@/types/authTypes"; +import type { Account } from "@/types/AccountType"; +import type { projectionDebtRequest } from "@/types/requestTypes"; + +const requestUrl = "http://localhost:3000/api/debts"; + +//Sends a request to get different debts the user has +export async function getDebt(session: AuthSession): Promise { + try { + //Sends a http request and waits for a response + const response = await fetch(requestUrl, { + method: "GET", + headers: { Authorization: `Bearer ${session.token}` }, + }); + + //Determine if we were able to retrieve user's data + if (!response.ok) { + //Failed to retrieve user data, return empty array + console.error("Error: Failed to retrieve users debts", response.status); + return []; + } + + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} + +//Post request, even tho it says get in the function +export async function getDebtProjection( + session: AuthSession, + request: projectionDebtRequest, +): Promise { + const url = "http://localhost:3000/predict-debt-payoff"; + + try { + //Create post request and wait for response + const response = await fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + Authorization: `Bearer ${session.token}`, + }, + body: JSON.stringify(request), + }); + + //Determine if our post was a success + if (!response.ok) { + console.error(response.status); + } + + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} diff --git a/app/client/src/api/Saving.ts b/app/client/src/api/Saving.ts new file mode 100644 index 0000000..6215858 --- /dev/null +++ b/app/client/src/api/Saving.ts @@ -0,0 +1,59 @@ +import { type Account } from "@/types/AccountType"; +import { type AuthSession } from "@/types/authTypes"; +import type { projectionSavingRequest } from "@/types/requestTypes"; +import { type savingProjectionResponseData } from "@/types/responseTypes"; +const requestUrl = "http://localhost:3000/api/savings"; + +//Sends a request to get different debts the user has +export async function getSaving(session: AuthSession): Promise { + try { + //Sends a http request and waits for a response + const response = await fetch(requestUrl, { + method: "GET", + headers: { Authorization: `Bearer ${session.token}` }, + }); + + //Determine if we were able to retrieve user's data + if (!response.ok) { + //Failed to retrieve user data, return empty array + console.error("Error: Failed to retrieve users debts", response.status); + return []; + } + + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} + +//Post request, even tho it says get in the function +export async function getSavingProjection( + session: AuthSession, + request: projectionSavingRequest, +): Promise { + const url = "http://localhost:3000/compound-interest"; + + try { + //Create post request and wait for response + const response = await fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + Authorization: `Bearer ${session.token}`, + }, + body: JSON.stringify(request), + }); + + //Determine if our post was a success + if (!response.ok) { + console.error(response.status); + return []; + } + + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 3eb91f7..3153898 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -8,7 +8,7 @@ const requestUrl = "http://localhost:3000/api/transactions"; //Sends a GET request to get the list of user transactions for the account export async function getTransactions( session: AuthSession, - financialAccount_id: string, + financialAccount_id: number, ): Promise { try { const response = await fetch( @@ -86,7 +86,7 @@ export async function putTranscations( } } -//DELETE /api/transactions/:id +//DELETE /api/transactions/ export async function deleteTransaction( session: AuthSession, diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index 578df72..02c6c75 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -36,14 +36,14 @@ export default function PopupForm({ let interest = undefined; const accountBalance = Number(balance); - if (accountType === accountCategory.SAVING) { + if ( + (accountType === "SAVING" || accountType === "CREDIT_CARD") && + formInput.subType + ) { subtype = formInput.subType; } - if ( - accountType === accountCategory.SAVING || - accountType === accountCategory.DEBT - ) { + if (accountType === accountCategory.SAVING) { interest = formInput.interest; } @@ -162,6 +162,7 @@ export default function PopupForm({ const [accountType, setAccountType] = useState( () => { if (edit && selectedAccount) { + console.log(selectedAccount.type); return selectedAccount.type as typeofAccount; } else { //Default value @@ -178,13 +179,18 @@ export default function PopupForm({ <>
- {edit ?

Edit Account

:

Create Account

} + {edit ? ( +

Edit Account

+ ) : ( +

Create Account

+ )} @@ -195,13 +201,14 @@ export default function PopupForm({ value={accountType} id="type" name="type" + className="formSelect" onChange={(event) => setAccountType(event.target.value as typeofAccount) } > {accountCat.map((category) => ( - ))} @@ -214,6 +221,7 @@ export default function PopupForm({ min="0" step="0.01" name="balance" + className="formInput" value={balance} onChange={(event) => handleCurrencyChange(event, setBalance)} onBlur={(event) => handleCurrencyBlur(event, balance, setBalance)} @@ -221,34 +229,46 @@ export default function PopupForm({ />

- {accountType === "DEBT" || accountType === "SAVING" ? ( - <> - - -

- - ) : null} - - {accountType === "SAVING" ? ( + {accountType === accountCategory.SAVING ? ( <> - { + handleChange(event); + setFormInput({ ...formInput, ["subType"]: "" }); + }} + > + +

) : null} + {accountType === accountCategory.CREDIT_CARD ? ( + <> + + + + ) : null} +
{edit ? ( diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx index 4fc5111..29aaab8 100644 --- a/app/client/src/components/AccountListCard.tsx +++ b/app/client/src/components/AccountListCard.tsx @@ -47,7 +47,7 @@ export default function Card({

- {account.type + " " + (account.subtype ? " " + account.type : "")} + {account.type + " " + (account.subtype ? " " + account.subtype : "")}

{Number(account.balance).toFixed(2)}

diff --git a/app/client/src/components/NavBar.tsx b/app/client/src/components/NavBar.tsx index 333fb7a..836e7bf 100644 --- a/app/client/src/components/NavBar.tsx +++ b/app/client/src/components/NavBar.tsx @@ -1,6 +1,7 @@ import { Sidebar } from "react-pro-sidebar"; import { Link, useLocation } from "react-router-dom"; import { CgHome } from "react-icons/cg"; +import { AiOutlineFundProjectionScreen } from "react-icons/ai"; import { MdCandlestickChart } from "react-icons/md"; import { FaSignOutAlt, FaTimes } from "react-icons/fa"; import { SIDEBAR_WIDTH } from "@/utils/constants"; @@ -16,6 +17,7 @@ function NavBar({ isOpen, onClose, onLogout }: NavigationBarProps) { const navItems = [ { to: "/dashboard", label: "Dashboard", icon: }, { to: "/markets", label: "Markets", icon: }, + {to:"/projection", label:"Projection", icon: } ]; return ( diff --git a/app/client/src/components/ProjectionGraph.tsx b/app/client/src/components/ProjectionGraph.tsx new file mode 100644 index 0000000..b6a3310 --- /dev/null +++ b/app/client/src/components/ProjectionGraph.tsx @@ -0,0 +1,90 @@ +import type { projectedDataResponse } from "@/types/responseTypes"; +import type { ChartData, ChartOptions } from "chart.js"; +import { Line } from "react-chartjs-2"; +import { Wallet } from "lucide-react"; +import NoItemState from "./NoItemState"; + +interface graphProp { + data: projectedDataResponse; + name: string; +} + +export default function ProjectionGraph({ data, name }: graphProp) { + const colors = [ + "rgba(34, 250, 94, 1)", + "rgba(206, 232, 11, 1)", + "rgba(197, 34, 34, 1)", + ]; + + const chartOptions: ChartOptions<"line"> = { + responsive: true, + plugins: { + legend: { + position: "top", + labels: { + font: { + size: 16, + weight: "bold", + }, + padding: 50, + }, + }, + }, + backgroundColor: "rgb(255, 255, 255, 0.8)", + scales: { + x: { + ticks: { + font: { + size: 14, + weight: "bold", // Set the font weight to bold + }, + }, + }, + y: { + beginAtZero: true, + ticks: { + font: { + size: 14, + weight: "bold", // Set the font weight to bold + }, + }, + }, + }, + }; + + console.log(data); + const chartData: ChartData<"line"> = { + labels: data.dateLabel, + datasets: data.lineInfo.map((line, index) => ({ + label: line.name, + data: line.data, + fill: true, + borderColor: colors[index % colors.length], + backgroundColor: colors[index % colors.length].replace("1)", "0.55)"), + borderWidth: 1, + })), + }; + + return ( + <> + {chartData ? ( +
+

{name}

+ +
+ ) : ( + <> + } + /> + + )} + + ); +} diff --git a/app/client/src/components/SelectAccount.tsx b/app/client/src/components/SelectAccount.tsx new file mode 100644 index 0000000..f2efbbf --- /dev/null +++ b/app/client/src/components/SelectAccount.tsx @@ -0,0 +1,35 @@ +import type { Account } from "@/types/AccountType"; +import React from "react"; + +interface selectProp { + accounts: Account[]; + selectedAccount: number; + handleSelectAccount: (event: React.ChangeEvent) => void; +} + +export default function SelectAccount({ + accounts, + selectedAccount, + handleSelectAccount, +}: selectProp) { + console.log(accounts); + return ( + <> + + + + ); +} diff --git a/app/client/src/components/SelectDebt.tsx b/app/client/src/components/SelectDebt.tsx new file mode 100644 index 0000000..1e21e03 --- /dev/null +++ b/app/client/src/components/SelectDebt.tsx @@ -0,0 +1,34 @@ +import { type Debt } from "@/types/Debt"; +import React, { type SetStateAction } from "react"; + +interface selectProp { + debts: Debt[]; + selectedDebt: number; + setSelectedDebt: React.Dispatch>; +} + +//Component that puts out a selection of all the user's debts +export default function SelectDebt({ + debts, + selectedDebt, + setSelectedDebt, +}: selectProp) { + return ( + <> + + + + ); +} diff --git a/app/client/src/components/TransactionCard.tsx b/app/client/src/components/TransactionCard.tsx index 6401b24..4e4c600 100644 --- a/app/client/src/components/TransactionCard.tsx +++ b/app/client/src/components/TransactionCard.tsx @@ -43,7 +43,9 @@ export default function Card({

{transaction.id}

-

{transaction.amount + " " + transaction.date}

+

{"$" + transaction.amount.toFixed(2)}

+

{"Date:" + transaction.date}

+

{"Type: " + transaction.category}



{transaction.description}

diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx index 85deae9..8e6fef8 100644 --- a/app/client/src/components/TransactionList.tsx +++ b/app/client/src/components/TransactionList.tsx @@ -6,6 +6,7 @@ import TransactionCard from "./TransactionCard"; import type { AuthSession } from "@/types/authTypes"; import { type Account } from "@/types/AccountType"; import { getUserAccounts } from "@/api/Account"; +import SelectAccount from "./SelectAccount"; interface listProp { session: AuthSession; @@ -13,7 +14,7 @@ interface listProp { export default function TransactionList({ session }: listProp) { const [userAccounts, setUserAccounts] = useState([]); - const [selectedAccount, setSelectedAccount] = useState(""); + const [selectedAccount, setSelectedAccount] = useState(0); const [accountTransactions, setAccountTransactions] = useState( [], ); @@ -85,20 +86,13 @@ export default function TransactionList({ session }: listProp) { <>
- - + , + ) => setSelectedAccount(Number(event.target.value))} + />
diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx index d26ca3a..6c0609d 100644 --- a/app/client/src/components/TransationForm.tsx +++ b/app/client/src/components/TransationForm.tsx @@ -9,6 +9,7 @@ import { type Transaction } from "../types/Transaction"; import { type Account } from "@/types/AccountType"; import { transactionCategory } from "@/enum/TransactionCategory"; import type { AuthSession } from "@/types/authTypes"; +import SelectAccount from "@/components/SelectAccount"; interface popupProp { toggle: () => void; @@ -215,24 +216,22 @@ export default function PopupForm({ <>
- {edit ?

Edit Transaction

:

Create Transaction

} + {edit ? ( +

Edit Transaction

+ ) : ( +

Create Transaction

+ )} + + {account && ( + , + ) => setSelectedAccount(Number(event.target.value))} + /> + )} - -

{selectedType == transactionCategory.INCOME ? ( @@ -244,6 +243,7 @@ export default function PopupForm({ id="other" type="text" value={other} + className="formInput" onChange={(event) => setOther(event.target.value)} >

@@ -252,6 +252,7 @@ export default function PopupForm({

- + setSelectedDate(event.target.value)} /> diff --git a/app/client/src/components/userForm.css b/app/client/src/components/userForm.css index 999dbe1..fb24786 100644 --- a/app/client/src/components/userForm.css +++ b/app/client/src/components/userForm.css @@ -1,33 +1,44 @@ .popupForm { - background-color: lightgrey; + background-color: rgba(14, 15, 15, 0.776); padding: 15px; place-content: start; + width: max-content; height: auto; + white-space: nowrap; } -input { +.formInput { margin-top: 2.5%; margin-bottom: 2.5%; margin-left: 1%; + margin-right: 1%; + background-color: rgba(0, 0, 0, 0.566); + padding-left: 2%; + border-color: black; + border-width: 2px; } -select { +.formSelect { margin-top: 2.5%; margin-bottom: 2.5%; margin-left: 1%; + background-color: rgba(0, 0, 0, 0.566); + padding-left: 2%; + border-color: black; + border-width: 2px; } .popup { color: black; - background-color: floralwhite; - padding: 15px; + background-color: rgb(34, 206, 66); + padding: 10px; border-radius: 5px; border-style: solid; border-width: 2px; - border-color: black; + border-color: rgba(34, 250, 94, 1); position: fixed; - top: 0; + top: 10%; bottom: 0; right: 35%; left: 35%; @@ -36,20 +47,29 @@ select { height: max-content; font-size: 10pt; + color: antiquewhite; justify-content: center; align-items: center; } -button { +.formButton { padding: 5px; border-width: 1px; border-color: black; border-radius: 5px; } +.formH2 { + font-weight: bold; + text-align: center; + padding-bottom: 2%; +} + .bottomButtons { + margin-top: 5px; display: flex; + color: black; } .bottomButtons :first-child { diff --git a/app/client/src/enum/AccountCategory.ts b/app/client/src/enum/AccountCategory.ts index 6b084df..b35f4ec 100644 --- a/app/client/src/enum/AccountCategory.ts +++ b/app/client/src/enum/AccountCategory.ts @@ -1,6 +1,7 @@ export const accountCategory = { - SAVING: "Saving", + SAVING: "Savings", CHEQUING: "Chequing", INVESTMENT: "Investment", - DEBT: "Debt", + CREDIT_CARD:"Credit Card", + UNCONFIRMED: "Unconfirmed" }; diff --git a/app/client/src/pages/ProjectionPage.tsx b/app/client/src/pages/ProjectionPage.tsx new file mode 100644 index 0000000..b520687 --- /dev/null +++ b/app/client/src/pages/ProjectionPage.tsx @@ -0,0 +1,536 @@ +import { + Chart, + PointElement, + LineElement, + ArcElement, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from "chart.js"; + +import type { AuthSession } from "../types/authTypes"; +import SelectAccount from "@/components/SelectAccount"; +import React, { useEffect, useState } from "react"; +import { type Account } from "@/types/AccountType"; +import { getDebt, getDebtProjection } from "@/api/Debt"; +import { getSaving, getSavingProjection } from "@/api/Saving"; +import { + type LineInfo, + type projectedDataResponse, +} from "@/types/responseTypes"; +import ProjectionGraph from "@/components/ProjectionGraph"; +import { TbGraph } from "react-icons/tb"; +import NoItemState from "@/components/NoItemState"; +import { handleCurrencyChange, handleCurrencyBlur } from "@/utils/handleInput"; +import type { + projectionDebtRequest, + projectionSavingRequest, +} from "@/types/requestTypes"; +import { + validateDebtProjection, + validateSavingProjection, +} from "@/utils/ValidateProjectionRequest"; +import { accountCategory } from "@/enum/AccountCategory"; + +Chart.register( + PointElement, + LineElement, + ArcElement, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +); + +type ProjectionProp = { + session: AuthSession; +}; + +function ProjectionPage({ session }: ProjectionProp) { + const [accounts, setAccounts] = useState([]); + const [debts, setDebts] = useState([]); + const [selectedAccount, setSelectedAccount] = useState(0); + const [selectedDebt, setSelectedDebt] = useState(0); + const [nextDueDate, setNextDueDate] = useState(""); + const [interest, setInterest] = useState(""); + const [amount, setAmount] = useState(""); + const [period, setPeriod] = useState(""); + const [minPay, setMinPay] = useState(""); + + const [selectedType, setSelectedType] = useState<"Saving" | "Debt">("Saving"); + + const [savingData, setSavingData] = useState< + projectedDataResponse | undefined + >(undefined); + const [debtData, setDebtData] = useState( + undefined, + ); + + const [debtRequest, setDebtRequest] = useState< + projectionDebtRequest | undefined + >(undefined); + const [savingRequest, setSavingRequest] = useState< + projectionSavingRequest | undefined + >(undefined); + + //Total amount of interest generated + const [totalInterest, setTotalInterest] = useState(0); + //Total amoutn of pay + const [totalPay, setTotalPay] = useState(0); + + const calculateProjection = () => { + const inputAmount = Number(amount); + const inputMinPay = Number(minPay); + const inputInterest = Number(interest); + const inputPeriod = Number(period); + + switch (selectedType) { + case "Debt": + if ( + validateDebtProjection( + selectedDebt, + inputAmount, + inputMinPay, + inputInterest, + nextDueDate, + inputPeriod, + ) + ) { + const newDebtRequest: projectionDebtRequest = { + id: selectedAccount.toString(), + category: accountCategory.CREDIT_CARD, + remainingAmount: inputAmount, + minimumPayment: inputMinPay, + interestRate: inputInterest / 100, + nextDueDate: nextDueDate, + period: inputPeriod, + }; + + setDebtRequest(newDebtRequest); + + getDebtProjection(session, newDebtRequest) + .then((data) => { + let dataTotalInterest = 0; + let dataTotalPay = 0; + const debtLineData: LineInfo = { + name: "Remaining Debt", + data: [], + }; + console.log(data); + if (data) { + const graphData: projectedDataResponse = { + lineInfo: [], + dateLabel: [], + }; + data.debtStages.map((stage) => { + debtLineData.data.push(stage.remainingDebt); + graphData.dateLabel.push(stage.installmentDate); + dataTotalInterest += stage.interestAmount; + dataTotalPay += stage.principalAmount; + }); + + graphData.lineInfo = [debtLineData]; + + setDebtData(graphData); + setTotalPay(dataTotalPay); + setTotalInterest(dataTotalInterest); + } + }) + .catch((error) => { + alert(error); + }); + } else { + alert("Please enter all fields"); + } + break; + + case "Saving": + if ( + validateSavingProjection( + selectedAccount, + inputInterest, + inputAmount, + inputMinPay, + inputPeriod, + ) + ) { + const newSavingRequest: projectionSavingRequest = { + financial_account_id: selectedAccount, + balance: inputAmount, + monthly_deposit: inputMinPay, + annual_interest_rate: inputInterest / 100, + time_frame: inputPeriod, + }; + + setSavingRequest(newSavingRequest); + + getSavingProjection(session, newSavingRequest) + .then((data) => { + console.log(data); + const graphData: projectedDataResponse = { + dateLabel: [], + lineInfo: [], + }; + const bestCase: LineInfo = { name: "Best Case", data: [] }; + const expectedCase: LineInfo = { + name: "Expected Case", + data: [], + }; + const worstCase: LineInfo = { name: "Worst Case", data: [] }; + + if (data) { + //Extract all the data from the response + data.map((datapoint) => { + graphData.dateLabel.push(datapoint.date); + bestCase.data.push(datapoint.accumulative_best_balance); + expectedCase.data.push( + datapoint.accumulative_expected_balance, + ); + worstCase.data.push(datapoint.accumulative_worst_balance); + }); + + graphData.lineInfo = [bestCase, expectedCase, worstCase]; + setSavingData(graphData); + } else { + alert("Failed to get the projection "); + } + }) + .catch(() => { + alert("Failed to get the projection"); + }); + } else { + alert("Please enter all fields"); + } + } + }; + + const handleTypeChange = (typeChange: "Saving" | "Debt") => { + setSelectedType(typeChange); + + if (selectedType === "Debt") { + if (debtRequest) { + //Set the fields to what was used in the debt projection graph + setSelectedAccount(Number(debtRequest.id)); + setAmount(debtRequest.remainingAmount.toFixed(2)); + setInterest(debtRequest.interestRate.toString()); + setMinPay(debtRequest.minimumPayment.toFixed(2)); + setNextDueDate(debtRequest.nextDueDate); + setPeriod(debtRequest.period.toString()); + } else { + setSelectedDebt(0); + setAmount(""); + setInterest(""); + setMinPay(""); + setPeriod(""); + setNextDueDate(""); + } + } else if (selectedType === "Saving") { + if (savingRequest) { + //Assigns the fields to what the projection of the saving account used + setSelectedAccount(savingRequest.financial_account_id); + setAmount(savingRequest.balance.toFixed(2)); + setInterest(savingRequest.annual_interest_rate.toString()); + setMinPay(savingRequest.monthly_deposit.toFixed(2)); + setPeriod(savingRequest.time_frame.toString()); + } else { + //Resets the fields + setSelectedAccount(0); + setAmount(""); + setInterest(""); + setMinPay(""); + setPeriod(""); + } + } + }; + + //Onchange handler for the select accounts + const handleSelectAccount = (event: React.ChangeEvent) => { + let target; + const id = Number(event.target.value); + + if (selectedType === "Debt") { + setSelectedDebt(id); + + target = debts.find((account) => account.id === id)?.balance; + } else if (selectedType === "Saving") { + setSelectedAccount(id); + + target = accounts.find((account) => account.id === id)?.balance; + } + + if (target) { + setAmount(target.toFixed(2)); + } else { + setAmount("0"); + } + }; + + //Handles on change of percentage + const handleNumberChange = ( + event: React.ChangeEvent, + setNumber: React.Dispatch>, + min: number, + max: number, + ) => { + let input = event.target.value; + let changeInterest; + const pattern = /^\d*\.?\d{0,2}$/; + + console.log(input); + console.log(pattern.test(input)); + + //Determine if the input follows the format/pattern + if (pattern.test(input) || input === "") { + input = input.replace(/^0+(?=\d)/, ""); + changeInterest = Number(input); + + if (changeInterest > max) { + changeInterest = max; + } + + if (changeInterest < min) { + changeInterest = min; + } + console.log(changeInterest); + setNumber(changeInterest.toString()); + } + }; + //Get the saving accounts + useEffect(() => { + if (selectedType === "Saving") { + getSaving(session) + .then((userAccounts) => { + console.log(userAccounts); + + //Detemrine accounts exist + if (userAccounts) { + setAccounts(userAccounts); + } + }) + .catch(() => { + //alert("Failed to get accounts"); + }); + } + }, [session, selectedType]); + + //Gets the debt accounts + useEffect(() => { + if (selectedType === "Debt") { + getDebt(session) + .then((userDebts) => { + console.log(userDebts); + + //Detemrine if there are any debts + if (userDebts) { + setDebts(userDebts); + } + }) + .catch(() => {}); + } + }, [session, selectedType]); + + const typeButton = [ + { key: "Saving", label: "Saving" }, + { key: "Debt", label: "Debt" }, + ].map(({ key, label }) => ( + + )); + + const glowLeft = ( +
+ ); + + const glowRight = ( +
+ ); + return ( +
+ {glowLeft} + {glowRight} +

Projection

+

+ Here you can view a projection of you're saving's or debt +

+ +

Select Saving Account or Debt

+ +
+ {typeButton} +
+ +
+ {selectedType === "Saving" ? ( + + ) : ( + + )} +
+ <> + + handleCurrencyChange(event, setAmount)} + onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} + /> + + + handleCurrencyChange(event, setMinPay)} + onBlur={(event) => handleCurrencyBlur(event, minPay, setMinPay)} + /> + + + { + handleNumberChange(event, setPeriod, 0, 100); + }} + /> + + + + { + handleNumberChange(event, setInterest, 0, 100); + }} + /> + + {selectedType === "Debt" && ( + <> +

+ + setNextDueDate(event.target.value)} + /> + + )} +
+ + +
+ +
+ {selectedType === "Saving" ? ( + savingData ? ( + <> + + + ) : ( + <> + } + /> + + ) + ) : null} + + {selectedType === "Debt" ? ( + debtData ? ( + <> + +
+ {"Total Amount Paid:$" + + totalPay.toFixed(2) + + " Total Interest Paid:$" + + totalInterest.toFixed(2)} +
+ + ) : ( + <> + } + /> + + ) + ) : null} +
+
+ ); +} + +export default ProjectionPage; diff --git a/app/client/src/types/Debt.ts b/app/client/src/types/Debt.ts new file mode 100644 index 0000000..6f1a3c5 --- /dev/null +++ b/app/client/src/types/Debt.ts @@ -0,0 +1,16 @@ +export type Debt = { + id: string; + name: string; + category: string; + remainingAmount: number; + minimumPayment: number; + interestRate?: number; + nextDueDate: string; // format: YYYY-MM-DD + period: number; // number of days between each payment installment +}; + +export type DebtStage = { + paidAmount: number; + remainingDebt: number; + installmentDate: string; // YYYY-MM-DD +}; diff --git a/app/client/src/types/requestTypes.ts b/app/client/src/types/requestTypes.ts new file mode 100644 index 0000000..01e3792 --- /dev/null +++ b/app/client/src/types/requestTypes.ts @@ -0,0 +1,17 @@ +export interface projectionDebtRequest { + id: string; + category: string; + remainingAmount: number; + minimumPayment: number; + interestRate: number; + nextDueDate: string; // format: YYYY-MM-DD + period: number; // number of days between each payment installment +} + +export interface projectionSavingRequest { + financial_account_id: number; + balance: number; + monthly_deposit: number; + annual_interest_rate: number; + time_frame: number; +} diff --git a/app/client/src/types/responseTypes.ts b/app/client/src/types/responseTypes.ts index b3ef6e0..6b4308f 100644 --- a/app/client/src/types/responseTypes.ts +++ b/app/client/src/types/responseTypes.ts @@ -3,3 +3,36 @@ export interface updateResponse { lastUpdated?: Date; id?: number; } + +export interface projectedDataResponse { + lineInfo: LineInfo[]; + dateLabel: string[]; +} + +export interface LineInfo { + data: number[]; + name: string; +} + +export type DebtPayoffResponse = { + id: string; + category: string; + minimumPayment: number; + interestRate: number; + debtStages: AdvancedDebtStage[]; +}; + +export type AdvancedDebtStage = { + id: number; + principalAmount: number; + interestAmount: number; + remainingDebt: number; + installmentDate: string; // YYYY-MM-DD +}; + +export interface savingProjectionResponseData { + accumulative_best_balance: number; + accumulative_expected_balance: number; + accumulative_worst_balance: number; + date: string; +} diff --git a/app/client/src/utils/ValidateProjectionRequest.ts b/app/client/src/utils/ValidateProjectionRequest.ts new file mode 100644 index 0000000..0d6349c --- /dev/null +++ b/app/client/src/utils/ValidateProjectionRequest.ts @@ -0,0 +1,35 @@ +export function validateDebtProjection( + id: number, + remainingAmount: number, + minimumPayment: number, + interestRate: number, + nextDueDate: string, + period: number, +) { + return ( + id && + remainingAmount >= 0 && + minimumPayment > 0 && + interestRate >= 0 && + interestRate <= 100 && + nextDueDate && + period + ); +} + +export function validateSavingProjection( + id: number, + interestRate: number, + balence: number, + monthly_deposit: number, + time_frame: number, +) { + return ( + id && + interestRate >= 0 && + interestRate <= 100 && + balence >= 0 && + monthly_deposit >= 0 && + time_frame > 0 + ); +} diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 096f10c..76428b5 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -58,17 +58,17 @@ services: market: condition: service_started - user-integration-test: - build: - context: ./services/ts - dockerfile: user/Dockerfile.integration.test - env_file: - - ./default_container.env - environment: - - JWT_SECRET + # user-integration-test: + # build: + # context: ./services/ts + # dockerfile: user/Dockerfile.integration.test + # env_file: + # - ./default_container.env + # environment: + # - JWT_SECRET - depends_on: - database: - condition: service_healthy - api-gateway: - condition: service_started + # depends_on: + # database: + # condition: service_healthy + # api-gateway: + # condition: service_started diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql index b5895b4..340e43e 100644 --- a/app/server/services/database/schema.sql +++ b/app/server/services/database/schema.sql @@ -154,8 +154,8 @@ CREATE TABLE finus.fixedInterestInvestment( ); #Populate lookup tables -INSERT INTO finus.financialAccountType (type) VALUES ('chequing'), ('savings'), ('credit_card'), ('investment'); -INSERT INTO finus.financialAccountSubtype (subtype) VALUES ('RRSP'), ('TFSA'), ('FHSA'), ('RESP'), ('RDSP'), ('loan'), ('na'); +INSERT INTO finus.financialAccountType (type) VALUES ('chequing'), ('savings'), ('credit_card'), ('investment'); +INSERT INTO finus.financialAccountSubtype (subtype) VALUES ('RRSP'), ('TFSA'), ('FHSA'), ('RESP'), ('RDSP'), ('Loan'), ('na'); -- loan is used for credit_card accounts that are for loans like mortgage and etc, this is used to track debt INSERT INTO finus.investmentType (type) VALUES ('fixedInterest'), ('stock'); #These have to match table names diff --git a/app/server/services/python/analytics/src/logic/debt.py b/app/server/services/python/analytics/src/logic/debt.py new file mode 100644 index 0000000..cf763ec --- /dev/null +++ b/app/server/services/python/analytics/src/logic/debt.py @@ -0,0 +1,48 @@ +import pandas as pd +from typing import List, Optional +from datetime import datetime, timedelta +from src.models.schemas import DebtPayoffRequest, DebtPayoffResponse, DebtPayoffStage, BadRequestError + + +def generate_debt_payoff_stages(dbr: DebtPayoffRequest) -> DebtPayoffResponse: + remaining_debt = dbr.remainingAmount + minimum_payment = dbr.minimumPayment + interest_rate = dbr.interestRate or 0 + + monthly_interest_rate = round(interest_rate / 12 / 100, 5) if interest_rate else 0 + + expected_date = datetime.strptime(dbr.nextDueDate, "%Y-%m-%d") + stages: List[DebtPayoffStage] = [] + + i = 1 + while remaining_debt > 0: + interest = remaining_debt * monthly_interest_rate + principal = minimum_payment - interest + + if principal > remaining_debt: + principal = remaining_debt + + if principal <= 0: + raise BadRequestError("Minimum payment is too low. Debt will never be paid off.") + + new_remaining = remaining_debt - principal + + stages.append(DebtPayoffStage( + id=i, + principalAmount=round(principal, 2), + interestAmount=round(interest, 2), + remainingDebt=round(new_remaining, 2), + installmentDate=expected_date.strftime("%Y-%m-%d") + )) + + remaining_debt = new_remaining + expected_date += timedelta(days=dbr.period) + i += 1 + + return DebtPayoffResponse( + id=dbr.id, + category=dbr.category, + minimumPayment=dbr.minimumPayment, + interestRate=interest_rate, + debtStages=stages, + ) diff --git a/app/server/services/python/analytics/src/logic/savings.py b/app/server/services/python/analytics/src/logic/savings.py index cb655bd..43166dd 100644 --- a/app/server/services/python/analytics/src/logic/savings.py +++ b/app/server/services/python/analytics/src/logic/savings.py @@ -1,6 +1,13 @@ import pandas as pd +import numpy as np from typing import List, Dict +from datetime import datetime, date +from dateutil.relativedelta import relativedelta +from fastapi import HTTPException # from ..queries import savings as savings_queries +from src.dependencies import get_db_connection +from src.models.schemas import ProjectedSavingsRequest, MonthlySavingGrowthRate, CompoundInterestResponse +from src.queries.savings import get_savings_transactions def calculate_savings_over_time( accounts: List[Dict], @@ -49,4 +56,154 @@ def calculate_savings_over_time( 'label': f'{period_name} Savings', 'data': [item['savings'] for item in savings_over_time] }] - } \ No newline at end of file + } + +# def get_monthly_balances(transactions: pd.DataFrame): +# df = transactions.copy() +# df['date'] = pd.to_datetime(df['date']) + +# # Sort by date +# df = df.sort_values('date') + +# # Group by month +# df['year_month'] = df['date'].dt.to_period('M') + +# monthly_sums = df.groupby('year_month')['amount'].sum().reset_index() + +# monthly_sums['year_month'] = monthly_sums['year_month'].dt.to_timestamp() + +# # Create full monthly range +# full_range = pd.date_range( +# start=monthly_sums['year_month'].min(), +# end=monthly_sums['year_month'].max(), +# freq='MS' # month start +# ) + +# monthly_sums = monthly_sums.set_index('year_month').reindex(full_range, fill_value=0) +# monthly_sums = monthly_sums.rename_axis('date').reset_index() + +# # Convert to cumulative balance +# monthly_sums['balance'] = monthly_sums['amount'].cumsum() +# print(monthly_sums) + +# return monthly_sums + +def get_monthly_balances(transactions: pd.DataFrame): + df = transactions.copy() + df['date'] = pd.to_datetime(df['date']) + + # Extract month only (1–12) + df['month'] = df['date'].dt.month + + # Initialize 12 months with no balance + monthly_totals = {month: 0 for month in range(1, 13)} + + # Aggregate ignoring year + for _, row in df.iterrows(): + monthly_totals[row['month']] += row['amount'] + + # Convert to DataFrame (ordered) + monthly_df = pd.DataFrame([ + {"month": m, "amount": monthly_totals[m]} + for m in range(1, 13) + ]) + + # Compute cumulative balance for each month + monthly_df['balance'] = monthly_df['amount'].cumsum() + + print(monthly_df) + + return monthly_df + +def compute_monthly_growth_rates(monthly_df: pd.DataFrame): + # Compute increase rate between months + monthly_df['growth_rate'] = monthly_df['balance'].pct_change() + + # Replace inf and -inf with NaN and drop NaN + monthly_df['growth_rate'].replace([np.inf, -np.inf], np.nan, inplace=True) + + # Replace NaN with 0 + monthly_df['growth_rate'].fillna(0, inplace=True) + + growth_rates = monthly_df['growth_rate'] + growth_rates = growth_rates.round(3) + + return growth_rates + +def generate_savings_growth_rate(transactions: List[Dict]) -> MonthlySavingGrowthRate: + df = pd.DataFrame(transactions) + + if df.empty: + return None + + monthly_balances = get_monthly_balances(df) + growth_rates = compute_monthly_growth_rates(monthly_balances) + + mean_growth = growth_rates.mean() + standard_deviation_growth = growth_rates.std() + + return MonthlySavingGrowthRate( + best_case=round(mean_growth + standard_deviation_growth, 2), + expected_case=round(mean_growth, 2), + worst_case=max(round(mean_growth - standard_deviation_growth, 2), 0) + ) + + +def calculate_compound_interest(request: ProjectedSavingsRequest) -> List[CompoundInterestResponse]: + connection = get_db_connection() + if not connection: + raise HTTPException(status_code=500, detail="Database connection failed") + + cursor = connection.cursor(dictionary=True) + transactions = get_savings_transactions(cursor, [request.financial_account_id]) + print(transactions) + if len(transactions) == 0: + return [] + + monthly_savings_rate = generate_savings_growth_rate(transactions) + print(monthly_savings_rate) + + best_rate = monthly_savings_rate.best_case + worst_rate = monthly_savings_rate.worst_case + expected_rate = monthly_savings_rate.expected_case + + worst_balance = request.balance + expected_balance = request.balance + best_balance = request.balance + + monthly_deposit = request.monthly_deposit + time_frame = request.time_frame + + results: List[CompoundInterestResponse] = [] + + months = time_frame * 12 + current_date = date.today() + + for _ in range(months): + # deposit money + worst_balance += monthly_deposit + expected_balance += monthly_deposit + best_balance += monthly_deposit + + # compute growth + worst_balance += worst_balance * worst_rate + expected_balance += expected_balance * expected_rate + best_balance += best_balance * best_rate + + # rounding + worst_balance = round(worst_balance, 2) + expected_balance = round(expected_balance, 2) + best_balance = round(best_balance, 2) + + results.append( + CompoundInterestResponse( + accumulative_worst_balance=worst_balance, + accumulative_expected_balance=expected_balance, + accumulative_best_balance=best_balance, + date=current_date.strftime("%B %Y") + ) + ) + + current_date += relativedelta(months=1) + + return results \ No newline at end of file diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py index 240a046..eb80e9b 100644 --- a/app/server/services/python/analytics/src/main.py +++ b/app/server/services/python/analytics/src/main.py @@ -1,14 +1,17 @@ from fastapi import FastAPI, Depends, Query, HTTPException from fastapi.middleware.cors import CORSMiddleware import pandas as pd +from typing import List, Dict import uvicorn import os + from src.dependencies import get_db_connection, get_current_user from src.utils.dates import period_calc from src.logic.budget import generate_budget, generate_budget_performance from src.queries import savings as savings_queries, incomeflow as incomeflow_queries -from src.logic import savings as savings_service, incomeflow as incomeflow_service +from src.logic import savings as savings_service, incomeflow as incomeflow_service, debt as debt_service +from src.models.schemas import BadRequestError, ProjectedSavingsRequest, ProjectedSavingsResponse, CompoundInterestResponse, DebtPayoffRequest app = FastAPI() @@ -116,6 +119,34 @@ async def get_budget( except Exception as e: print(e) raise HTTPException(status_code=500, detail=str(e)) + +@app.post('/predict-debt-payoff') +async def predict_debt_payoff( + requestBody: DebtPayoffRequest, + user_id: int = Depends(get_current_user), +): + try: + print(f"Generating predicted debt payoff for user {user_id}") + print(requestBody) + return debt_service.generate_debt_payoff_stages(requestBody) + except BadRequestError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + print(e) + raise HTTPException(status_code=500, detail=str(e)) + +@app.post('/compound-interest') +async def compount_interest( + requestBody: ProjectedSavingsRequest, + user_id: int = Depends(get_current_user), +) -> List[CompoundInterestResponse]: + try: + print(f"Generating compound interests for user {user_id}") + print(requestBody) + return savings_service.calculate_compound_interest(requestBody) + except Exception as e: + print(e) + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/server/services/python/analytics/src/models/schemas.py b/app/server/services/python/analytics/src/models/schemas.py index bd8b43c..be89b8f 100644 --- a/app/server/services/python/analytics/src/models/schemas.py +++ b/app/server/services/python/analytics/src/models/schemas.py @@ -1,3 +1,4 @@ +from numpy import number from pydantic import BaseModel from typing import List, Literal, Optional from datetime import datetime @@ -51,4 +52,52 @@ class BudgetPerformanceItem(BaseModel): class BudgetPerformanceResponse(BaseModel): categories: List[str] budgetAmounts: List[float] - actualAmounts: List[float] \ No newline at end of file + actualAmounts: List[float] + +class DebtPayoffRequest(BaseModel): + id: str + category: str + remainingAmount: float + minimumPayment: float + interestRate: Optional[float] = 0 + nextDueDate: str # YYYY-MM-DD + period: int # days + +class DebtPayoffStage(BaseModel): + id: int + principalAmount: float + interestAmount: float + remainingDebt: float + installmentDate: str + +class DebtPayoffResponse(BaseModel): + id: str + category: str + minimumPayment: float + interestRate: float + debtStages: List[DebtPayoffStage] + +class ProjectedSavingsRequest(BaseModel): + financial_account_id: int + balance: float + monthly_deposit: float + annual_interest_rate: float | None # annual interest rate in percentage + time_frame: int # in years +class MonthlySavingGrowthRate(BaseModel): + best_case: float + expected_case: float + worst_case: float +class ProjectedSavingsResponse(BaseModel): + balance: float + monthly_contribution: float + interest_rate: float | None # annual interest rate in percentage + time_frame: int # in years + stat: List[MonthlySavingGrowthRate] +class CompoundInterestResponse(BaseModel): + accumulative_best_balance: float + accumulative_expected_balance: float + accumulative_worst_balance: float + date: str + +class BadRequestError(Exception): + pass \ No newline at end of file diff --git a/app/server/services/python/analytics/src/queries/savings.py b/app/server/services/python/analytics/src/queries/savings.py index e8b3bfd..b7909f0 100644 --- a/app/server/services/python/analytics/src/queries/savings.py +++ b/app/server/services/python/analytics/src/queries/savings.py @@ -1,5 +1,4 @@ from typing import List, Dict -import mysql.connector def get_savings_accounts(cursor, user_id: int) -> List[Dict]: cursor.execute(""" diff --git a/app/server/services/python/analytics/test/conftest.py b/app/server/services/python/analytics/test/conftest.py index 5374bc6..1dfc278 100644 --- a/app/server/services/python/analytics/test/conftest.py +++ b/app/server/services/python/analytics/test/conftest.py @@ -1,7 +1,8 @@ import pytest +import pandas as pd from unittest.mock import patch, MagicMock from datetime import datetime, timedelta -from src.models.schemas import BudgetCategory, BudgetResponse +from src.models.schemas import BudgetCategory, BudgetResponse, DebtPayoffRequest from src.utils.trans_cat_classifier import CategoryClassifier import jwt import os @@ -62,6 +63,30 @@ def mock_db_queries(): yield mock +@pytest.fixture +def mock_savings_accounts(): + return [ + {'id': 1, 'balance': 5000}, + {'id': 2, 'balance': 3000}, + ] + +@pytest.fixture +def mock_savings_transactions(): + base_date = datetime.now() - timedelta(days=30) + return [ + {'financialAccount_id': 1, 'amount': 100, 'date': base_date + timedelta(days=5)}, + {'financialAccount_id': 1, 'amount': 200, 'date': base_date + timedelta(days=10)}, + {'financialAccount_id': 2, 'amount': -50, 'date': base_date + timedelta(days=7)}, + {'financialAccount_id': 2, 'amount': 150, 'date': base_date + timedelta(days=15)}, + {'financialAccount_id': 1, 'amount': -75, 'date': base_date + timedelta(days=20)}, + ] +@pytest.fixture +def mock_savings_transactions_by_financial_account(): + return pd.DataFrame([ + {"financialAccountId": "1", "date": "2026-01-10", "amount": 100}, + {"financialAccountId": "1", "date": "2025-01-20", "amount": 50}, + {"financialAccountId": "1", "date": "2026-02-01", "amount": 200}, + ]) @pytest.fixture def mock_incomeflow_transactions(): @@ -78,7 +103,35 @@ def mock_incomeflow_transactions(): {'amount': -100, 'category': 'entertainment', 'date': base_date + timedelta(days=8)}, {'amount': -400, 'category': 'utilities', 'date': base_date + timedelta(days=12)}, ] +@pytest.fixture +def mock_transactions_with_same_amount(): + return [ + {"date": "2026-01-01", "amount": 100}, + {"date": "2026-02-01", "amount": 100}, + {"date": "2026-03-01", "amount": 100}, + {"date": "2026-04-01", "amount": 100}, + {"date": "2026-05-01", "amount": 100}, + ] +@pytest.fixture +def mock_monthly_diff(): + return pd.DataFrame({ + "month": [1, 2, 3], + "amount": [0, 0, 100], + "balance": [0, 0, 100] + }) + +@pytest.fixture +def mock_debt_payoff_request(): + return DebtPayoffRequest( + id="1", + category="credit_card", + remainingAmount=1000.0, + minimumPayment=200.0, + interestRate=12.0, + nextDueDate="2024-01-01", + period=30 + ) @pytest.fixture def mock_env_vars(): diff --git a/app/server/services/python/analytics/test/test_debts.py b/app/server/services/python/analytics/test/test_debts.py new file mode 100644 index 0000000..54b9d25 --- /dev/null +++ b/app/server/services/python/analytics/test/test_debts.py @@ -0,0 +1,52 @@ +import pytest +from src.logic.debt import generate_debt_payoff_stages +from src.models.schemas import BadRequestError, DebtPayoffRequest, DebtPayoffResponse + +class TestDebts: + def test_generate_debt_payoff(self, mock_debt_payoff_request): + result = generate_debt_payoff_stages(mock_debt_payoff_request) + + assert result.id == mock_debt_payoff_request.id + assert result.category == mock_debt_payoff_request.category + assert len(result.debtStages) > 0 + + # final debt should be zero + assert result.debtStages[-1].remainingDebt == 0 + + + def test_generate_debt_payoff_no_interest(self, mock_debt_payoff_request): + mock_debt_payoff_request.interestRate = 0 + + result = generate_debt_payoff_stages(mock_debt_payoff_request) + + # No interest should be charged + for stage in result.debtStages: + assert stage.interestAmount == 0 + + assert len(result.debtStages) == 5 + + + def test_last_payment_adjustment(self, mock_debt_payoff_request): + mock_debt_payoff_request.remainingAmount = 450 + mock_debt_payoff_request.minimumPayment = 200 + mock_debt_payoff_request.interestRate = 0 + + result = generate_debt_payoff_stages(mock_debt_payoff_request) + + last_stage = result.debtStages[-1] + + # Last payment should exactly clear the debt + assert last_stage.remainingDebt == 0 + assert last_stage.principalAmount <= mock_debt_payoff_request.minimumPayment + + + def test_min_payment_too_low(self, mock_debt_payoff_request): + mock_debt_payoff_request.minimumPayment = 1 + mock_debt_payoff_request.interestRate = 100 # Very high interest + + with pytest.raises(BadRequestError) as exc: + generate_debt_payoff_stages(mock_debt_payoff_request) + + assert "too low" in str(exc.value) + + diff --git a/app/server/services/python/analytics/test/test_savings.py b/app/server/services/python/analytics/test/test_savings.py index a1f94de..53e4ed2 100644 --- a/app/server/services/python/analytics/test/test_savings.py +++ b/app/server/services/python/analytics/test/test_savings.py @@ -1,9 +1,9 @@ +from src.models.schemas import ProjectedSavingsRequest import pytest import pandas as pd -from datetime import datetime -from unittest.mock import patch, MagicMock -from src.logic.savings import calculate_savings_over_time from datetime import datetime, timedelta +from unittest.mock import patch, MagicMock +from src.logic.savings import calculate_savings_over_time, calculate_compound_interest, generate_savings_growth_rate, compute_monthly_growth_rates, get_monthly_balances class TestSavings: @@ -119,4 +119,37 @@ def test_calculate_savings_over_time_no_accounts(self): # Should return empty structure assert result['labels'] == [] - assert result['datasets'][0]['data'] == [] \ No newline at end of file + assert result['datasets'][0]['data'] == [] + + def test_get_monthly_balances(self, mock_savings_transactions_by_financial_account): + + result = get_monthly_balances(mock_savings_transactions_by_financial_account) + + # January = 150, February = 200 + assert result.loc[result['month'] == 1, 'amount'].values[0] == 150 + assert result.loc[result['month'] == 2, 'amount'].values[0] == 200 + + # cumulative balance + assert result.loc[result['month'] == 1, 'balance'].values[0] == 150 + assert result.loc[result['month'] == 2, 'balance'].values[0] == 350 + + def test_compute_growth_rates_handles_nan_and_inf(self, mock_monthly_diff): + + growth_rates = compute_monthly_growth_rates(mock_monthly_diff) + + # Should not contain NaN or inf + assert not growth_rates.isnull().any() + assert not (growth_rates == float("inf")).any() + + def test_generate_growth_rate(self, mock_transactions_with_same_amount): + + result = generate_savings_growth_rate(mock_transactions_with_same_amount) + + assert result is not None + assert result.best_case >= result.expected_case + assert result.expected_case >= result.worst_case + + def test_generate_growth_rate_empty(self): + result = generate_savings_growth_rate([]) + + assert result is None \ No newline at end of file diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index b286e95..0313249 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -171,6 +171,42 @@ app.use( }), ); +//compound interest from analytics service +app.use( + createProxyMiddleware({ + pathFilter: ["/compound-interest"], + target: process.env.ANALYTICS_SERVICE_ADDR, + changeOrigin: true, + }), +); + +//debt payoff prediction from analytics service +app.use( + createProxyMiddleware({ + pathFilter: ["/predict-debt-payoff"], + target: process.env.ANALYTICS_SERVICE_ADDR, + changeOrigin: true, + }), +); + +//debt-related request +app.use( + createProxyMiddleware({ + pathFilter: "/api/debts", + target: process.env.USER_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { "^/api/debts": "/debts" }, + }), +); +//savings-related request +app.use( + createProxyMiddleware({ + pathFilter: "/api/savings", + target: process.env.USER_SERVICE_ADDR, + changeOrigin: true, + pathRewrite: { "^/api/savings": "/savings" }, + }), +); // market search, quote, and history endpoints from market service app.use( createProxyMiddleware({ diff --git a/app/server/services/ts/user/integration/setup.ts b/app/server/services/ts/user/integration/setup.ts index 83f9098..de537fa 100644 --- a/app/server/services/ts/user/integration/setup.ts +++ b/app/server/services/ts/user/integration/setup.ts @@ -55,6 +55,8 @@ export async function createAuthenticatedAccount( } const token = login.body.token as string; + + // Create the account const account = await request(baseUrl) .post("/api/accounts") .set("Authorization", `Bearer ${token}`) @@ -65,6 +67,9 @@ export async function createAuthenticatedAccount( value: accountOverrides?.value ?? 1000, subtype: accountOverrides?.subtype ?? "na", }); + + console.log("Account creation response:", account.status, account.body); + if (account.status !== 200 || !account.body.id) { lastFailure = `account=${account.status} ${JSON.stringify(account.body)}`; await sleep(500); @@ -78,6 +83,7 @@ export async function createAuthenticatedAccount( } catch (error) { lastFailure = error instanceof Error ? error.message : "unknown setup error"; + console.log("Setup error:", lastFailure); await sleep(500); } } diff --git a/app/server/services/ts/user/src/CheckUser.ts b/app/server/services/ts/user/src/CheckUser.ts new file mode 100644 index 0000000..3a92f34 --- /dev/null +++ b/app/server/services/ts/user/src/CheckUser.ts @@ -0,0 +1,39 @@ +import type { RowDataPacket, Connection } from "mysql2/promise"; + +//Checks the user is the owner of the account +export async function checkUserId( + db: Connection, + userId: number, + accountId: number, +): Promise { + let result = false; + try { + //need the user account's profile id first. This should be just a single item returned unless stretch feature 7 is implemented + const [profileRows] = await db.query( + `SELECT profile_id FROM finusAccount_profile WHERE account_id = ?`, + [userId], + ); + + if (!profileRows || (profileRows as RowDataPacket[]).length === 0) { + throw new Error("User profile not found"); + } + + const profileId = (profileRows as RowDataPacket[])[0].profile_id; + + //Checks if the profile has an account with that id + const [rows] = await db.query( + `SELECT 1 FROM profile_financialAccount + WHERE profile_id =? AND financialAccount_id =?`, + [profileId, accountId], + ); + + console.log(rows.length); + if (rows.length > 0) { + result = true; + } + } catch (error) { + console.error(error); + } + + return result; +} diff --git a/app/server/services/ts/user/src/DataConversion.ts b/app/server/services/ts/user/src/DataConversion.ts new file mode 100644 index 0000000..b5a35f9 --- /dev/null +++ b/app/server/services/ts/user/src/DataConversion.ts @@ -0,0 +1,5 @@ + +//C0j +export function convertToDateTime(date: string) { + return date.slice(0, 19).replace("T", " "); +} diff --git a/app/server/services/ts/user/src/handleJWT.ts b/app/server/services/ts/user/src/handleJWT.ts index c150c66..fc55203 100644 --- a/app/server/services/ts/user/src/handleJWT.ts +++ b/app/server/services/ts/user/src/handleJWT.ts @@ -1,16 +1,17 @@ import jwt from "jsonwebtoken"; import type { Request } from "express"; +import { UnauthorizedAccessError } from "./types/UnauthorizedAccess.js"; const JWT_SECRET = process.env.JWT_SECRET; export const authenticateJWT = (req: Request) => { const authHeader = req.headers.authorization; if (!authHeader) { - throw new Error("Authorization header missing"); + throw new UnauthorizedAccessError("Authorization header missing"); } const parts = authHeader.split(" "); if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { - throw new Error("Invalid authorization header format"); + throw new UnauthorizedAccessError("Invalid authorization header format"); } const token = parts[1]; //add as any if it doesn't work @@ -24,7 +25,7 @@ export const authenticateJWT = (req: Request) => { const userId = decoded.sub; //console.log("user id: " + userId); if (!userId) { - throw new Error("User ID not found in token"); + throw new UnauthorizedAccessError("User ID not found in token"); } return userId; diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index 31302da..5017b71 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -21,6 +21,8 @@ import { getGoalCountByProfileId } from "./queries/goals.ts"; import { accountsRouter } from "./routes/account.js"; import { profilesRouter } from "./routes/profile.js"; import { transactionsRouter } from "./routes/transaction.js"; +import { debtRouter } from "./routes/debt.ts"; +import { savingRouter } from "./routes/saving.ts"; const app = express(); app.use(buildCorsConfig()); @@ -28,21 +30,33 @@ onExit(async () => await server.close()); app.use(express.json()); -app.use( - (req: express.Request, res: express.Response, next: express.NextFunction) => { - // console.log("USER Incoming request: " + req.method + " " + req.url); - // console.log(req.body); - next(); - }, -); +app.use((req, res, next) => { + console.log("USER Incoming request: " + req.method + " " + req.url); + console.log(req.body); + next(); +}); app.use("/accounts", accountsRouter); app.use("/transactions", transactionsRouter); app.use("/profiles", profilesRouter); +app.use("/debts", debtRouter); +app.use("/savings", savingRouter); const server = app.listen(PORT, () => { console.log(`User Service running on port ${PORT}`); }); +process.on("SIGTERM", () => cleanup); + + async function cleanup() { + try{ + server.close() + await pool.end() + } catch(error){ + console.log(error) + } +} + +app.get("/charts/expenses", async (req, res) => { process.on("SIGTERM", () => server.close()); app.get( @@ -123,7 +137,7 @@ app.get( }, ); -export { generateDateRange }; //for testing purposes only +//export { generateDateRange }; //for testing purposes only // Goal stuff is below------------------------------------------------------ @@ -291,3 +305,5 @@ app.delete("/goals", async (req: express.Request, res: express.Response) => { } } }); + +export { generateDateRange }; //for testing purposes only diff --git a/app/server/services/ts/user/src/logic/debt.ts b/app/server/services/ts/user/src/logic/debt.ts new file mode 100644 index 0000000..a2ee725 --- /dev/null +++ b/app/server/services/ts/user/src/logic/debt.ts @@ -0,0 +1,18 @@ +import type { FinancialAccountRequest } from "../types/FinancialAccountRequest.ts"; +import { addDebt, findDebtsBy } from "../queries/debt.ts"; +import type { DebtInfoResponse } from "../types/DebtInfoResponse.ts"; +import { BadRequestError } from "../types/BadRequestError.ts"; +import { getConnectionPool } from "@/sqlUtil.ts"; + +const db = getConnectionPool() +export async function getDebts(userId: string): Promise { + return await findDebtsBy(db, userId) +} +export async function createNewDebt(debt: FinancialAccountRequest, userId: string): Promise { + try{ + return await addDebt(db, debt,userId) + } catch (err) { + console.error("Error creating a new saving account: ", err); + throw err; + } +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/logic/saving.ts b/app/server/services/ts/user/src/logic/saving.ts new file mode 100644 index 0000000..4148f3f --- /dev/null +++ b/app/server/services/ts/user/src/logic/saving.ts @@ -0,0 +1,41 @@ +import { addSavingAccount, findSavingsBy } from "../queries/saving.ts"; +import type { SavingInfoResponse } from "../types/SavingInfoResponse.ts"; +import type { FinancialAccountRequest } from "../types/FinancialAccountRequest.ts"; +import { findTransactionsBy } from "../queries/transactions.ts"; +import type { TransactionDto } from "../types/TransactionDto.ts"; +import { BadRequestError } from "../types/BadRequestError.ts"; +import { getConnectionPool } from "@/sqlUtil.ts"; +const db = getConnectionPool(); +export async function getSavingAccount(userId: string): Promise { + return await findSavingsBy(db, userId); +} +export async function getSavingAccountTransactionBy(financialAccountId: string | undefined): Promise { + if (!financialAccountId) + throw new BadRequestError("Missing financial account id") + + const transactions = await findTransactionsBy(db, financialAccountId); + const transationDtos : TransactionDto[] = transactions.map((transation) => ({ + id: transation.id, + amount: transation.amount, + category: transation.category, + description: transation.description, + sender: transation.sender, + recipient: transation.recipient, + date: transation.date ? + new Date(transation.date) + .toISOString() + .replace("T", " ") + .replace(/\.\d{3}Z$/, "") + : + "N/A" + })) + return transationDtos; +} +export async function createSavingAccount(savingInfo: FinancialAccountRequest, userId: string) : Promise{ + try { + return await addSavingAccount(db, savingInfo, userId); + } catch (err) { + console.error("Error creating a new saving account: ", err); + throw err; + } +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/queries/debt.ts b/app/server/services/ts/user/src/queries/debt.ts new file mode 100644 index 0000000..e9fb460 --- /dev/null +++ b/app/server/services/ts/user/src/queries/debt.ts @@ -0,0 +1,74 @@ +import { getConnectionPool } from "@/sqlUtil.ts"; +import { RowDataPacket } from "mysql2"; +import type { PoolConnection, ResultSetHeader, Pool } from "mysql2/promise"; +import type { DebtInfoResponse } from "../types/DebtInfoResponse.ts"; +import type { FinancialAccountRequest } from "../types/FinancialAccountRequest.ts"; +import { FinancialAccountType } from "../types/FinancialAccountType.ts"; + +export async function findDebtsBy(db: Pool, userId: string) : Promise{ + const query = ` + SELECT fa.id, fa.name, fa.balance, fa.subtype, fa.last_updated + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ? + AND fa.type = 'Credit Card' + AND fa.subtype = 'Loan' + GROUP BY fa.id + `; + + const [rows] = await db.execute(query, [userId]); + console.log(rows) + const result: DebtInfoResponse[] = rows.map((row) => ({ + id: Number(row.id), + name: String(row.name), + balance: Number(row.balance), + type: FinancialAccountType.CREDIT, + subtype: String(row.subtype), + lastUpdated: row.last_updated + ? new Date(row.last_updated).toISOString() + : "N/A", + })); + return result; +} +export async function addDebt(db: Pool, newDebt: FinancialAccountRequest, userId: string): Promise { + + const connection : PoolConnection = await db.getConnection(); + await connection.beginTransaction(); + + try { + const query = `INSERT INTO finus.financialAccount (name, type, balance, value, subtype) VALUES (?, ?, ?, ?, ?)`; + const params = [newDebt.name, newDebt.type, newDebt.balance, newDebt.value, newDebt.subtype]; + + const [result] = await connection.execute(query, params); + const financialAccountId = result.insertId; + + const profileQuery = `SELECT * from profile p JOIN finusAccount_profile fp on p.id = fp.profile_id where fp.account_id = ?` + const [profiles] = await connection.execute(profileQuery, [userId]) + + if (profiles.length > 0) { + const values = profiles.map((p) => [p.id, financialAccountId]); + const linkQuery = `INSERT INTO finus.profile_financialAccount (profile_id, financialAccount_id) VALUES ?`; + await connection.query(linkQuery, [values]); + } + + await connection.commit(); + + + console.log("From db", result) + return { + id: Number(financialAccountId), + name: String(newDebt.name), + balance: Number(newDebt.balance), + subtype: String(newDebt.subtype), + lastUpdated: new Date().toISOString().replace("T", " ") ?? "N/A", + } as DebtInfoResponse; + } catch (err) { + // Rollback if anything fails + await connection.rollback(); + throw err; + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/queries/saving.ts b/app/server/services/ts/user/src/queries/saving.ts new file mode 100644 index 0000000..841c97a --- /dev/null +++ b/app/server/services/ts/user/src/queries/saving.ts @@ -0,0 +1,77 @@ +import { getConnectionPool } from "@/sqlUtil.ts"; +import { RowDataPacket } from "mysql2"; +import type { PoolConnection, ResultSetHeader, Pool } from "mysql2/promise"; +import type { SavingInfoResponse } from "../types/SavingInfoResponse.ts"; +import type { FinancialAccountRequest } from "../types/FinancialAccountRequest.ts"; +import { FinancialAccountType } from "../types/FinancialAccountType.ts"; +const db = getConnectionPool(); + +export async function findSavingsBy(db: Pool, userId: string) : Promise{ + const query = ` + SELECT fa.id, fa.name, fa.balance, fa.subtype, fa.last_updated + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.profile p ON pfa.profile_id = p.id + JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id + WHERE uap.account_id = ? + AND fa.type = 'Savings' + GROUP BY fa.id + `; + + const [rows] = await db.execute(query, [userId]); + console.log(rows) + const result: SavingInfoResponse[] = rows.map((row) => ({ + id: Number(row.id), + name: String(row.name), + balance: Number(row.balance), + type: FinancialAccountType.SAVINGS, + subtype: String(row.subtype), + lastUpdated: row.last_updated + ? new Date(row.last_updated).toISOString() + : "N/A", + })); + return result; +} + +export async function addSavingAccount(db: Pool, newSavings: FinancialAccountRequest, userId: string) : Promise{ + + const connection : PoolConnection = await db.getConnection(); + await connection.beginTransaction(); + + try { + const { name, type, balance, value, subtype } = newSavings; + const query = `INSERT INTO finus.financialAccount (name, type, balance, value, subtype) VALUES (?, ?, ?, ?, ?)`; + const params = [name, type, balance, value, subtype]; + + const [result] = await connection.execute(query, params); + const financialAccountId = result.insertId; + + const profileQuery = `SELECT * from profile p JOIN finusAccount_profile fp on p.id = fp.profile_id where fp.account_id = ?` + const [profiles] = await connection.execute(profileQuery, [userId]) + + if (profiles.length > 0) { + const values = profiles.map((p) => [p.id, financialAccountId]); + const linkQuery = `INSERT INTO finus.profile_financialAccount (profile_id, financialAccount_id) VALUES ?`; + await connection.query(linkQuery, [values]); + } + + await connection.commit(); + + + console.log("From db", result) + return { + id: Number(financialAccountId), + name: String(newSavings.name), + balance: Number(newSavings.balance), + subtype: String(newSavings.subtype), + lastUpdated: new Date().toISOString().replace("T", " ") ?? "N/A", + } as SavingInfoResponse; + } catch (err) { + // Rollback if anything fails + await connection.rollback(); + throw err; + } finally { + connection.release(); + } +} + diff --git a/app/server/services/ts/user/src/queries/transactions.ts b/app/server/services/ts/user/src/queries/transactions.ts index 1ebbe58..88e8243 100644 --- a/app/server/services/ts/user/src/queries/transactions.ts +++ b/app/server/services/ts/user/src/queries/transactions.ts @@ -21,6 +21,20 @@ export async function getAllTransactionsQuery( return rows; } +export async function findTransactionsBy( + db: Pool, + financialAccountId: string, +): Promise { + const query = ` + SELECT * FROM finus.transaction t + WHERE t.financialAccount_id = ? + ORDER BY t.date DESC + `; + + const [rows] = await db.execute(query, [financialAccountId]); + return rows; +} + export async function getDateCategoryTransactionsQuery( pool: Pool, profileId: number, diff --git a/app/server/services/ts/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts index 316483e..3a2ed8e 100644 --- a/app/server/services/ts/user/src/routes/account.ts +++ b/app/server/services/ts/user/src/routes/account.ts @@ -3,20 +3,25 @@ //TODO import db connection import { Router } from "express"; import type { Request, Response } from "express"; -import type { ResultSetHeader } from "mysql2"; -import { getConnectionPool } from "@/sqlUtil"; +import type { + ResultSetHeader, + PoolConnection, + RowDataPacket, +} from "mysql2/promise"; import type { financialAccount } from "@/types.js"; import { authenticateJWT } from "../handleJWT.js"; +import { checkUserId } from "../CheckUser.ts"; +import { pool } from "../db.ts"; //import { authenticateJWT } from "../handleJWT.js"; //import { error } from "node:console"; export const accountsRouter = Router(); -const db = getConnectionPool(); - accountsRouter.post("/", async (req: Request, res: Response) => { + let connection: PoolConnection | undefined; try { + connection = await pool.getConnection(); const { name, type, balance, value, subtype } = req.body; const last_updated = new Date(); @@ -47,16 +52,28 @@ accountsRouter.post("/", async (req: Request, res: Response) => { .json({ error: "Not authorized to create an account" }); } - const [result] = await db.query( + //need the user account's profile id first. This should be just a single item returned unless stretch feature 7 is implemented + const [profileRows] = await connection.query( + `SELECT profile_id FROM finusAccount_profile WHERE account_id = ?`, + [userId], + ); + + if (!profileRows || (profileRows as RowDataPacket[]).length === 0) { + return res.status(404).json({ error: "User profile not found" }); + } + + const profileId = (profileRows as RowDataPacket[])[0].profile_id; + + const [result] = await connection.query( `INSERT INTO financialAccount (name, type, balance, value, last_updated, subtype) VALUES (?, ?, ?, ?, ?, ?)`, [name, type, balance, value, last_updated, subtype ?? null], ); - await db.query( + await connection.query( `INSERT INTO profile_financialAccount (profile_id, financialAccount_id) - VALUES (?,?)`, - [userId, result.insertId], + VALUES (?, ?)`, + [profileId, result.insertId], ); console.log("Created account " + name); @@ -68,12 +85,16 @@ accountsRouter.post("/", async (req: Request, res: Response) => { } catch (err) { console.error("Account creation failed", err); return res.status(500).json({ error: "Account creation failed" }); + } finally { + if (connection) { + connection.release(); + } } }); accountsRouter.get("/", async (req: Request, res: Response) => { let userId; - + let connection: PoolConnection | undefined; try { userId = authenticateJWT(req); } catch (err) { @@ -83,11 +104,25 @@ accountsRouter.get("/", async (req: Request, res: Response) => { if (userId) { try { - const [rows] = await db.query( + connection = await pool.getConnection(); + + //need the user account's profile id first. This should be just a single item returned unless stretch feature 7 is implemented + const [profileRows] = await connection.query( + `SELECT profile_id FROM finusAccount_profile WHERE account_id = ?`, + [userId], + ); + + if (!profileRows || (profileRows as RowDataPacket[]).length === 0) { + return res.status(404).json({ error: "User profile not found" }); + } + + const profileId = (profileRows as RowDataPacket[])[0].profile_id; + + const [rows] = await connection.query( `SELECT * FROM financialAccount JOIN profile_financialAccount pfa ON financialAccount.id = pfa.financialAccount_id WHERE pfa.profile_id = ?`, - [userId], + [profileId], ); console.log(rows); @@ -96,12 +131,17 @@ accountsRouter.get("/", async (req: Request, res: Response) => { } catch (err) { console.error("Failed to retrieve user's account", err); return res.status(500).json({ error: "Failed to retrieve account(s)" }); + } finally { + if (connection) { + connection.release(); + } } } }); accountsRouter.put("/", async (req: Request, res: Response) => { let userId; + let connection: PoolConnection | undefined; //Checks if was given by the requests if (isAccount(req.body)) { @@ -121,20 +161,22 @@ accountsRouter.put("/", async (req: Request, res: Response) => { .json({ error: "User not authorized to update account" }); } - //Check if the user is the owner of the account that's bineg updated - if (!(await checkUserId(userId, account.id))) { - console.error("User is not own of the account"); - return res - .status(401) - .json({ error: "User is not authorized to update accounts" }); - } - try { + connection = await pool.getConnection(); + + //Check if the user is the owner of the account that's bineg updated + if (!(await checkUserId(connection, userId, account.id))) { + console.error("User is not own of the account"); + return res + .status(401) + .json({ error: "User is not authorized to update accounts" }); + } + const { id, name, type, balance, value, subtype } = req.body; const last_updated = new Date(); - await db.query( + await connection.query( `UPDATE financialAccount SET name = ?, type = ?, balance = ?, value = ?, last_updated = ?, subtype = ? WHERE id= ?`, @@ -149,12 +191,16 @@ accountsRouter.put("/", async (req: Request, res: Response) => { } catch (err) { console.error("Failed to update user's account", err); return res.status(500).json({ error: "Failed to update user's account" }); + } finally { + if (connection) { + connection.release(); + } } }); accountsRouter.delete("/", async (req: Request, res: Response) => { let userId; - + let connection: PoolConnection | undefined; const { id } = req.body; try { @@ -174,52 +220,38 @@ accountsRouter.delete("/", async (req: Request, res: Response) => { .json({ error: "Bad request: No account was givens" }); } - //Checks if user is owner of the acount - if (!(await checkUserId(userId, id))) { - console.error("User cannot delete account they didn't create"); - return res - .status(401) - .json({ error: "User not authorized to delete this account" }); - } - try { - await db.query( + connection = await pool.getConnection(); + + //Checks if user is owner of the acount + if (!(await checkUserId(connection, userId, id))) { + console.error("User cannot delete account they didn't create"); + return res + .status(401) + .json({ error: "User not authorized to delete this account" }); + } + + await connection.query( `DELETE FROM financialAccount WHERE id=?`, [id], ); + await connection.query( + "DELETE FROM transaction WHERE financialAccount_id=?", + [id], + ); + return res.status(200).json({ message: "Account successfully deleted" }); } catch (err) { console.error("Failed to delete user's account", err); return res.status(500).json({ error: "Failed to delete user's account" }); - } -}); - -//Checks the user is the owner of the account -async function checkUserId( - userId: number, - accountId: number, -): Promise { - let result = false; - try { - //Checks if the profile has an account with that id - const [rows] = await db.query( - `SELECT 1 FROM profile_financialAccount - WHERE profile_id =? AND financialAccount_id =?`, - [userId, accountId], - ); - - console.log(rows.length); - if (rows.length > 0) { - result = true; + } finally { + if (connection) { + connection.release(); } - } catch (error) { - console.error(error); } - - return result; -} +}); function isAccount(reqBody: unknown): reqBody is financialAccount { //Checks if it exists and is an object diff --git a/app/server/services/ts/user/src/routes/debt.ts b/app/server/services/ts/user/src/routes/debt.ts new file mode 100644 index 0000000..8fbe77c --- /dev/null +++ b/app/server/services/ts/user/src/routes/debt.ts @@ -0,0 +1,44 @@ +import { Router } from "express"; +import type { Request, Response } from "express"; +import type { ResultSetHeader } from "mysql2"; +import { pool } from "../db.ts"; +import type { financialAccount } from "@/types.js"; +import { authenticateJWT } from "../handleJWT.js"; +import { UnauthorizedAccessError } from "../types/UnauthorizedAccess.ts"; +import { createNewDebt, getDebts } from "../logic/debt.ts"; +import { BadRequestError } from "../types/BadRequestError.ts"; +export const debtRouter = Router(); + +debtRouter.get("/", async (req: Request, res: Response) => { + try { + const userId = authenticateJWT(req); + getDebts(userId).then((debts) => { + res.status(200).json(debts); + }); + } catch (err: any) { + switch (err.constructor) { + case UnauthorizedAccessError: + console.error("User is not authorized to access debt", err); + return res.status(401).json({ error: "User is not authorized to access debt" }); + default: + console.error("Debt access failed", err); + res.status(500).json({ error: "Debt access failed" }); + } + } +}); +debtRouter.post("/", async (req: Request, res: Response) => { + try { + const userId = authenticateJWT(req); + const newDebt = await createNewDebt(req.body, userId); + res.status(201).json(newDebt); + } catch (err: any) { + switch (err.constructor) { + case UnauthorizedAccessError: + console.error("User is not authorized to create debt", err); + return res.status(401).json({ error: "User is not authorized to create debt" }); + default: + console.error("Debt creation failed", err); + res.status(500).json({ error: "Debt creation failed" }); + } + } +}); \ No newline at end of file diff --git a/app/server/services/ts/user/src/routes/saving.ts b/app/server/services/ts/user/src/routes/saving.ts new file mode 100644 index 0000000..9384027 --- /dev/null +++ b/app/server/services/ts/user/src/routes/saving.ts @@ -0,0 +1,76 @@ +import { Router } from "express"; +import type { Request, Response } from "express"; +import { authenticateJWT } from "../handleJWT.js"; +import { UnauthorizedAccessError } from "../types/UnauthorizedAccess.ts"; +import { createSavingAccount, getSavingAccount, getSavingAccountTransactionBy } from "../logic/saving.ts"; +import { BadRequestError } from "../types/BadRequestError.ts"; +export const savingRouter = Router(); + +savingRouter.get("/", async (req: Request, res: Response) => { + try { + const userId = authenticateJWT(req); + const savings = await getSavingAccount(userId) + res.status(200).json(savings) + } catch (err: any) { + switch (err.constructor) { + case UnauthorizedAccessError: + console.error("User is not authorized to access saving", err); + return res.status(401).json({ error: "User is not authorized to access saving" }); + default: + console.error("Saving access failed", err); + res.status(500).json({ error: "Saving access failed" }); + } + } +}); +savingRouter.get("/:financialAccountId/transactions", async (req: Request, res: Response) => { + try { + const userId = authenticateJWT(req); + const financialAccountId = req.params.financialAccountId?.toString() + console.log("Financial account id: ", financialAccountId) + const transactions = await getSavingAccountTransactionBy(financialAccountId) + res.status(200).json(transactions) + } catch (err: any) { + switch (err.constructor) { + case BadRequestError: + return res.status(400).json({error: err.message}) + case UnauthorizedAccessError: + console.error("User is not authorized to access saving", err); + return res.status(401).json({ error: "User is not authorized to access saving" }); + default: + console.error("Saving access failed", err); + res.status(500).json({ error: "Saving access failed" }); + } + } +}); +savingRouter.post("/", async (req: Request, res: Response) => { + try { + const userId = authenticateJWT(req); + const savingAccount = await createSavingAccount(req.body, userId); + res.status(201).json({ data: savingAccount }); + } catch (err: any) { + switch (err.constructor) { + case UnauthorizedAccessError: + console.error("User is not authorized to create saving", err); + return res.status(401).json({ error: "User is not authorized to create saving" }); + default: + console.error("Saving creation failed", err); + res.status(500).json({ error: "Saving creation failed" }); + } + } +}); +savingRouter.post("/projected", async (req: Request, res: Response) => { + try { + const userId = authenticateJWT(req); + console.log(req.body) + res.status(200).json({ data: "Projected savings calculated successfully" }); + } catch (err: any) { + switch (err.constructor) { + case UnauthorizedAccessError: + console.error("User is not authorized to create saving", err); + return res.status(401).json({ error: "User is not authorized to create saving" }); + default: + console.error("Saving creation failed", err); + res.status(500).json({ error: "Saving creation failed" }); + } + } +}); \ No newline at end of file diff --git a/app/server/services/ts/user/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts index 26e7413..3318ee6 100644 --- a/app/server/services/ts/user/src/routes/transaction.ts +++ b/app/server/services/ts/user/src/routes/transaction.ts @@ -1,19 +1,22 @@ // transaction routes for creating and updating transactions import { Router } from "express"; import type { Request, Response } from "express"; -import type { ResultSetHeader } from "mysql2"; -import { getConnectionPool } from "@/sqlUtil"; +import type { PoolConnection, ResultSetHeader } from "mysql2/promise"; import { authenticateJWT } from "../handleJWT.js"; import type { Transaction } from "@/types.js"; +import { checkUserId } from "../CheckUser.ts"; +import { convertToDateTime } from "../DataConversion.ts"; +import { pool as connection, pool } from "../db.ts"; export const transactionsRouter = Router(); -const db = getConnectionPool(); // Get all transactions for a specific financial account transactionsRouter.get("/", async (req: Request, res: Response) => { let userId; + let connection: PoolConnection | undefined; try { + connection = await pool.getConnection(); const financialAccount_id = Number(req.query.financialAccount_id); if (!financialAccount_id) { @@ -29,7 +32,7 @@ transactionsRouter.get("/", async (req: Request, res: Response) => { .json({ error: "User not authorized to create a transaction" }); } - if (!(await checkUserId(userId, financialAccount_id))) { + if (!(await checkUserId(connection, userId, financialAccount_id))) { console.error( "User is not authorized to create a transaction on this account", ); @@ -38,7 +41,7 @@ transactionsRouter.get("/", async (req: Request, res: Response) => { }); } - const [rows] = await db.query( + const [rows] = await connection.query( `SELECT * FROM transaction WHERE financialAccount_id = ?`, [financialAccount_id], ); @@ -54,7 +57,9 @@ transactionsRouter.get("/", async (req: Request, res: Response) => { transactionsRouter.post("/", async (req: Request, res: Response) => { let userId; + let connection: PoolConnection | undefined; try { + connection = await pool.getConnection(); const { financialAccount_id, amount, @@ -76,7 +81,7 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { .json({ error: "User not authorized to create a transaction" }); } - if (!(await checkUserId(userId, financialAccount_id))) { + if (!(await checkUserId(connection, userId, financialAccount_id))) { console.error( "User is not authorized to create a transaction on this account", ); @@ -85,7 +90,7 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { }); } - const [result] = await db.query( + const [result] = await connection.query( `INSERT INTO transaction (financialAccount_id, amount, description, sender, recipient, date, category ) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ @@ -106,14 +111,20 @@ transactionsRouter.post("/", async (req: Request, res: Response) => { } catch (err) { console.error("Transaction creation failed", err); res.status(500).json({ error: "Transaction creation failed" }); + } finally { + if (connection) { + connection.release(); + } } }); // Update an existing transaction transactionsRouter.put("/", async (req: Request, res: Response) => { let userId; + let connection: PoolConnection | undefined; try { + connection = await pool.getConnection(); const { id, financialAccount_id, @@ -135,7 +146,7 @@ transactionsRouter.put("/", async (req: Request, res: Response) => { .json({ error: "User not authorized to create a transaction" }); } - if (!(await checkUserId(userId, financialAccount_id))) { + if (!(await checkUserId(connection, userId, financialAccount_id))) { console.error( "User is not authorized to create a transaction on this account", ); @@ -144,7 +155,7 @@ transactionsRouter.put("/", async (req: Request, res: Response) => { }); } - await db.query( + await connection.query( `UPDATE transaction SET amount=?, description=?, sender=?, recipient=?, date=? WHERE id=?`, @@ -161,6 +172,10 @@ transactionsRouter.put("/", async (req: Request, res: Response) => { res.json({ message: "Transaction successfully updated" }); } catch (err) { console.error("Transaction update failed", err); + } finally { + if (connection) { + connection.release(); + } } }); @@ -184,14 +199,14 @@ transactionsRouter.delete("/", async (req: Request, res: Response) => { .json({ error: "User not authorized to delete transaction" }); } - if (!(await checkUserId(userId, financialAccount_id))) { + if (!(await checkUserId(connection, userId, financialAccount_id))) { console.error("User is not authorized to delete this transaction"); return res .status(401) .json({ error: "User not authorized to delete this transaction" }); } - await db.query(`DELETE FROM transaction WHERE id=?`, [id]); + await connection.query(`DELETE FROM transaction WHERE id=?`, [id]); res.status(200).json({ message: "Transaction deleted" }); } catch (err) { @@ -205,8 +220,9 @@ transactionsRouter.post( "/csvTransaction", async (req: Request, res: Response) => { let userId; - + let connection: PoolConnection | undefined; try { + connection = await pool.getConnection(); const { financialAccount_id, transactions } = req.body; if (!financialAccount_id || !Array.isArray(transactions)) { @@ -224,7 +240,7 @@ transactionsRouter.post( } // Check account ownership - if (!(await checkUserId(userId, financialAccount_id))) { + if (!(await checkUserId(connection, userId, financialAccount_id))) { console.error("User is not authorized to import into this account"); return res.status(401).json({ error: "User not authorized to import into this account", @@ -269,8 +285,6 @@ transactionsRouter.post( .json({ error: "No valid transactions to import" }); } - const connection = await db.getConnection(); - try { await connection.beginTransaction(); @@ -324,33 +338,37 @@ transactionsRouter.post( } catch (err) { console.error("CSV transaction import failed", err); res.status(500).json({ error: "CSV transaction import failed" }); + } finally { + if (connection) { + connection.release(); + } } }, ); //Checks the user is the owner of the account -async function checkUserId( - userId: number, - accountId: number, -): Promise { - let result = false; - try { - //Checks if the profile has an account with that id - const [rows] = await db.query( - `SELECT * FROM profile_financialAccount - WHERE profile_id =? AND financialAccount_id =?`, - [userId, accountId], - ); - - console.log(rows.length); - result = rows.length > 0; - } catch (error) { - console.error(error); - } - - return result; -} - -function convertToDateTime(date: string) { - return date.slice(0, 19).replace("T", " "); -} +// async function checkUserId( +// userId: number, +// accountId: number, +// ): Promise { +// let result = false; +// try { +// //Checks if the profile has an account with that id +// const [rows] = await db.query( +// `SELECT * FROM profile_financialAccount +// WHERE profile_id =? AND financialAccount_id =?`, +// [userId, accountId], +// ); + +// console.log(rows.length); +// result = rows.length > 0; +// } catch (error) { +// console.error(error); +// } + +// return result; +// } + +// function convertToDateTime(date: string) { +// return date.slice(0, 19).replace("T", " "); +// } diff --git a/app/server/services/ts/user/src/types/BadRequestError.ts b/app/server/services/ts/user/src/types/BadRequestError.ts new file mode 100644 index 0000000..1d6736a --- /dev/null +++ b/app/server/services/ts/user/src/types/BadRequestError.ts @@ -0,0 +1,7 @@ +export class BadRequestError extends Error { + constructor(message: string) { + super(message); + this.name = "UnauthorizedAccessError"; + Object.setPrototypeOf(this, BadRequestError.prototype); + } +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/DebtInfoResponse.ts b/app/server/services/ts/user/src/types/DebtInfoResponse.ts new file mode 100644 index 0000000..9155782 --- /dev/null +++ b/app/server/services/ts/user/src/types/DebtInfoResponse.ts @@ -0,0 +1,9 @@ +import type { FinancialAccountType } from "./FinancialAccountType.ts"; +export interface DebtInfoResponse { + id: number; + balance: number; + name: string; + type: FinancialAccountType.CREDIT; + subtype: string; + lastUpdated?: string; +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/ExpectedDebtDueDate.ts b/app/server/services/ts/user/src/types/ExpectedDebtDueDate.ts new file mode 100644 index 0000000..8142464 --- /dev/null +++ b/app/server/services/ts/user/src/types/ExpectedDebtDueDate.ts @@ -0,0 +1,8 @@ +export interface ExpectedDebtDueDate { + id: number; + category: string; + minimumPayment: number; + remainingAmount: number; + nextDueDate: string; + period: number; // in days +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/ExpectedDebtDueDateResponse.ts b/app/server/services/ts/user/src/types/ExpectedDebtDueDateResponse.ts new file mode 100644 index 0000000..679e15c --- /dev/null +++ b/app/server/services/ts/user/src/types/ExpectedDebtDueDateResponse.ts @@ -0,0 +1,7 @@ +export interface ExpectedDebtDueDateResponse { + id: number; + category: string; + minimumPayment: number; + amountPerInstallment: number[]; + installmentDates: string[]; // Array of expected due dates in ISO format +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/FinancialAccountRequest.ts b/app/server/services/ts/user/src/types/FinancialAccountRequest.ts new file mode 100644 index 0000000..1128825 --- /dev/null +++ b/app/server/services/ts/user/src/types/FinancialAccountRequest.ts @@ -0,0 +1,20 @@ +import type { FinancialAccountType } from "./FinancialAccountType.ts"; +import type { SavingAccountType } from "./SavingAccountType.ts"; + +export interface FinancialAccountRequest { + /*amount: number; + dueDate: string; + category: string; + interestRate?: number; + installment?: { + totalInstallments: number; + period: number; //in days + initialDate: string; + minimumPayment?: number; + };*/ + name: string; + type: FinancialAccountType; + balance: number; + value: number; + subtype: SavingAccountType | "Loan" | "n/a"; +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/FinancialAccountType.ts b/app/server/services/ts/user/src/types/FinancialAccountType.ts new file mode 100644 index 0000000..9a71622 --- /dev/null +++ b/app/server/services/ts/user/src/types/FinancialAccountType.ts @@ -0,0 +1,7 @@ +export enum FinancialAccountType { + SAVINGS = 'Savings', + CREDIT = 'Credit Card', + INVESTMENT = 'Investment', + CHEQUING = 'Chequing', + UNCONFIRMED = 'Unconfirmed', +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/InvestmentType.ts b/app/server/services/ts/user/src/types/InvestmentType.ts new file mode 100644 index 0000000..372e2c3 --- /dev/null +++ b/app/server/services/ts/user/src/types/InvestmentType.ts @@ -0,0 +1,4 @@ +export enum InvestmentType { + STOCK = "stock", + FIXED_INTEREST = "fixedInterest" +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/SavingAccountType.ts b/app/server/services/ts/user/src/types/SavingAccountType.ts new file mode 100644 index 0000000..e79bdab --- /dev/null +++ b/app/server/services/ts/user/src/types/SavingAccountType.ts @@ -0,0 +1,7 @@ +export enum SavingAccountType { + RRSP = 'RRSP', + TFSA = 'TFSA', + FHSA = 'FHSA', + RESP = 'RESP', + RDSP = 'RDSP' +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/SavingInfoRequest.ts b/app/server/services/ts/user/src/types/SavingInfoRequest.ts new file mode 100644 index 0000000..fa50fef --- /dev/null +++ b/app/server/services/ts/user/src/types/SavingInfoRequest.ts @@ -0,0 +1,8 @@ +import type { SavingAccountType } from "./SavingAccountType.ts"; + +export interface SavingInfoRequest { + name: string, + balance: number, + accountType: SavingAccountType, + interestRate?: number, +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/SavingInfoResponse.ts b/app/server/services/ts/user/src/types/SavingInfoResponse.ts new file mode 100644 index 0000000..d3a22af --- /dev/null +++ b/app/server/services/ts/user/src/types/SavingInfoResponse.ts @@ -0,0 +1,11 @@ +import type { FinancialAccountType } from "./FinancialAccountType.ts"; +import type { SavingAccountType } from "./SavingAccountType.ts"; + +export interface SavingInfoResponse { + id: number; + balance: number; + name: string; + type: FinancialAccountType.SAVINGS; + subtype: SavingAccountType; + lastUpdated?: string; +} \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/TransactionDto.ts b/app/server/services/ts/user/src/types/TransactionDto.ts new file mode 100644 index 0000000..df22358 --- /dev/null +++ b/app/server/services/ts/user/src/types/TransactionDto.ts @@ -0,0 +1,3 @@ +import type { Transaction } from "./Transaction.ts"; + +export interface TransactionDto extends Omit{}; \ No newline at end of file diff --git a/app/server/services/ts/user/src/types/UnauthorizedAccess.ts b/app/server/services/ts/user/src/types/UnauthorizedAccess.ts new file mode 100644 index 0000000..546fdad --- /dev/null +++ b/app/server/services/ts/user/src/types/UnauthorizedAccess.ts @@ -0,0 +1,8 @@ +export class UnauthorizedAccessError extends Error { + constructor(message: string) { + super(message); // Call the constructor of the base class `Error` + this.name = "UnauthorizedAccessError"; // Set the error name to your custom error class name + // Set the prototype explicitly to maintain the correct prototype chain + Object.setPrototypeOf(this, UnauthorizedAccessError.prototype); + } +} \ No newline at end of file diff --git a/app/server/services/ts/user/test/logic_debts.test.ts b/app/server/services/ts/user/test/logic_debts.test.ts new file mode 100644 index 0000000..c69a25e --- /dev/null +++ b/app/server/services/ts/user/test/logic_debts.test.ts @@ -0,0 +1,212 @@ +import { vi, describe, expect, it, beforeEach } from "vitest"; +import type { Pool, PoolConnection } from "mysql2/promise"; +import { findDebtsBy, addDebt } from "../src/queries/debt.ts"; +import { FinancialAccountType } from "../src/types/FinancialAccountType.ts"; +import type { FinancialAccountRequest } from "../src/types/FinancialAccountRequest.ts"; + +describe("findDebtsBy", () => { + const subtype = "Loan" + it("should return mapped debt accounts", async () => { + // Setup fake data + const mockRows = [ + { + id: "1", + name: "Debt A", + balance: 1000, + subtype: subtype, + last_updated: "2022-10-17T00:00:00.000Z", + }, + { + id: "2", + name: "Debt B", + balance: 9000, + subtype: subtype, + last_updated: "2026-02-08T19:00:00.000Z", + }, + ]; + + const mockDb = { + execute: vi.fn().mockResolvedValue([mockRows]), + } as unknown as Pool; + + const result = await findDebtsBy(mockDb, "30"); + + // Assert + expect(mockDb.execute).toHaveBeenCalled(); + + expect(result.length).toEqual(2) + expect(result[0]).toEqual( + { + id: 1, + name: "Debt A", + balance: 1000, + type: FinancialAccountType.CREDIT, + subtype: subtype, + lastUpdated: new Date("2022-10-17T00:00:00.000Z").toISOString(), + }, + ); + expect(result[1]).toEqual( + { + id: 2, + name: "Debt B", + balance: 9000, + type: FinancialAccountType.CREDIT, + subtype: subtype, + lastUpdated: new Date("2026-02-08T19:00:00.000Z").toISOString(), + }, + ); + }); + + it("should return empty array if no rows", async () => { + const mockDb = { + execute: vi.fn().mockResolvedValue([[]]), + } as unknown as Pool; + + const result = await findDebtsBy(mockDb, "20"); + + expect(result).toEqual([]); + }); + + it("should handle null/invalid last_updated safely", async () => { + const mockRows = [ + { + id: "3", + name: "Debt C", + balance: 500, + type: FinancialAccountType.CREDIT, + subtype: subtype, + last_updated: null, + }, + ]; + + const mockDb = { + execute: vi.fn().mockResolvedValue([mockRows]), + } as unknown as Pool; + + const result = await findDebtsBy(mockDb, "50"); + + expect(result[0]?.lastUpdated).toBe("N/A"); + }); + + it("should call DB with correct query and userId", async () => { + const mockDb = { + execute: vi.fn().mockResolvedValue([[]]), + } as unknown as Pool; + + await findDebtsBy(mockDb, "1"); + + expect(mockDb.execute).toHaveBeenCalledWith( + expect.stringContaining("SELECT"), + ["1"] + ); + }); +}); + +describe("addDebt", () => { + let mockConnection: Partial; + let mockDb: Partial; + + beforeEach(() => { + mockConnection = { + beginTransaction: vi.fn(), + commit: vi.fn(), + rollback: vi.fn(), + release: vi.fn(), + execute: vi.fn(), + query: vi.fn(), + }; + + mockDb = { + getConnection: vi.fn().mockResolvedValue(mockConnection), + }; + }); + + const newDebts : FinancialAccountRequest = { + name: "Testing Debt Financial Account", + type: FinancialAccountType.CREDIT, + balance: 1000, + value: 1000, + subtype: "Loan", + }; + + const userId = "10" + + it("should insert debt account and link profiles", async () => { + (mockConnection.execute as any) + // INSERT financialAccount + .mockResolvedValueOnce([{ insertId: 10 }]) + // SELECT profiles + .mockResolvedValueOnce([[{ id: 1 }, { id: 2 }]]); + + (mockConnection.query as any).mockResolvedValueOnce([]); + + const result = await addDebt( + mockDb as Pool, + newDebts, + userId + ); + + // Assert + expect(mockConnection.beginTransaction).toHaveBeenCalled(); + expect(mockConnection.execute).toHaveBeenCalledTimes(2); + expect(mockConnection.query).toHaveBeenCalled(); + expect(mockConnection.commit).toHaveBeenCalled(); + expect(mockConnection.release).toHaveBeenCalled(); + + expect(result.id).toBe(10); + expect(result.name).toBe("Testing Debt Financial Account"); + expect(result.balance).toBe(1000); + }); + + // Should not insert into link table due to no profile found + it("should skip linking if no profiles found", async () => { + (mockConnection.execute as any) + .mockResolvedValueOnce([{ insertId: 20 }]) + .mockResolvedValueOnce([[]]); // no profiles + + const result = await addDebt( + mockDb as Pool, + newDebts, + userId + ); + + expect(mockConnection.query).not.toHaveBeenCalled(); + expect(mockConnection.commit).toHaveBeenCalled(); + expect(result.id).toBe(20); + }); + + // Should rollback on error + it("should rollback if any query fails", async () => { + (mockConnection.execute as any) + .mockResolvedValueOnce([{ insertId: 30 }]) + .mockRejectedValueOnce(new Error("DB error")); // fail on profile query + + await expect( + addDebt(mockDb as Pool, newDebts, userId) + ).rejects.toThrow("DB error"); + + expect(mockConnection.rollback).toHaveBeenCalled(); + expect(mockConnection.commit).not.toHaveBeenCalled(); + expect(mockConnection.release).toHaveBeenCalled(); + }); + + // Should call queries with correct params + it("should call INSERT with correct values", async () => { + (mockConnection.execute as any) + .mockResolvedValueOnce([{ insertId: 40 }]) + .mockResolvedValueOnce([[]]); + + await addDebt(mockDb as Pool, newDebts, "user-1"); + + expect(mockConnection.execute).toHaveBeenCalledWith( + expect.stringContaining("INSERT INTO finus.financialAccount"), + [ + newDebts.name, + newDebts.type, + newDebts.balance, + newDebts.value, + newDebts.subtype, + ] + ); + }); +}); \ No newline at end of file diff --git a/app/server/services/ts/user/test/logic_savings.test.ts b/app/server/services/ts/user/test/logic_savings.test.ts new file mode 100644 index 0000000..3c14579 --- /dev/null +++ b/app/server/services/ts/user/test/logic_savings.test.ts @@ -0,0 +1,121 @@ +import { vi, describe, expect, it, beforeEach } from "vitest"; +import * as transactionQueries from "../src/queries/transactions.ts"; +import { getSavingAccountTransactionBy } from "../src/logic/saving.ts"; + +vi.mock("../src/queries/transactions", () => ({ + findTransactionsBy: vi.fn(), +})); + +describe("getSavingAccountTransactionBy", () => { + const mockFindTransactionsBy = transactionQueries.findTransactionsBy as vi.Mock; + beforeEach(() => { + vi.clearAllMocks(); + }); + + // Valid transactions + it("should return mapped TransactionDto", async () => { + const mockTransactions = [ + { + id: 1, + financialAccountId: 1, + amount: 45.30, + category: "Income", + description: "Bi-weekly Tips", + sender: "Huy", + recipient: "Restaurant Owner Jeff", + date: "2026-03-20T18:30:00.456Z", + }, + { + id: 2, + financialAccountId: 50, + amount: 9000, + category: "Salary", + description: "Monthly salary", + sender: "Hospital Manager", + recipient: "Huy", + date: "2026-03-15T12:00:00.789Z", + }, + ]; //mocking data returned from querying + + mockFindTransactionsBy.mockResolvedValue(mockTransactions); + + const result = await getSavingAccountTransactionBy("1"); + + expect(transactionQueries.findTransactionsBy).toHaveBeenCalledWith( + expect.anything(), + "1" + ); + + expect(result).toEqual([ + { + id: 1, + amount: 45.30, + category: "Income", + description: "Bi-weekly Tips", + sender: "Huy", + recipient: "Restaurant Owner Jeff", + date: "2026-03-20 18:30:00", + }, + { + id: 2, + amount: 9000, + category: "Salary", + description: "Monthly salary", + sender: "Hospital Manager", + recipient: "Huy", + date: "2026-03-15 12:00:00", + }, + ]); + }); + + // Empty transactions + it("should return empty array if no transactions", async () => { + mockFindTransactionsBy.mockResolvedValue([]); + + const result = await getSavingAccountTransactionBy("1"); + + expect(result).toEqual([]); + }); + + // Empty date + it("should assign N/A to missing date", async () => { + const mockTransactions = [ + { + id: 2, + financialAccountId: 10, + amount: 5000, + category: "Student fee savings", + description: "savings to pay annual student fee", + sender: "Parents", + recipient: "Huy", + date: null, + }, + ]; + + mockFindTransactionsBy.mockResolvedValue( + mockTransactions + ); + + const result = await getSavingAccountTransactionBy("1"); + + expect(result[0].date).toBe("N/A"); + }); + + // Throw error for missing financialAccountId + it("should throw error if financialAccountId is missing", async () => { + await expect( + getSavingAccountTransactionBy(undefined) + ).rejects.toThrow("Missing financial account id"); + }); + + // Triggering db error + it("should throw if findTransactionsBy fails", async () => { + mockFindTransactionsBy.mockRejectedValue( + new Error("DB error") + ); + + await expect( + getSavingAccountTransactionBy("1") + ).rejects.toThrow("DB error"); + }); +}); \ No newline at end of file diff --git a/app/server/services/ts/user/test/logic_transactions.test.ts b/app/server/services/ts/user/test/logic_transactions.test.ts index 0eb7c26..d4fe83c 100644 --- a/app/server/services/ts/user/test/logic_transactions.test.ts +++ b/app/server/services/ts/user/test/logic_transactions.test.ts @@ -8,6 +8,7 @@ import { createEmptyTransactions, createTransactionsWithMissingFields, } from "./factories/transaction.factory.ts"; +import type { Transaction } from "../src/types/Transaction.ts"; vi.mock("../src/queries/transactions", () => ({ getAllTransactionsQuery: vi.fn(), @@ -85,3 +86,4 @@ describe("getTransactionsData", () => { }); }); }); + diff --git a/app/server/services/ts/user/test/queries_savings.test.ts b/app/server/services/ts/user/test/queries_savings.test.ts new file mode 100644 index 0000000..da07a06 --- /dev/null +++ b/app/server/services/ts/user/test/queries_savings.test.ts @@ -0,0 +1,211 @@ +import { vi, describe, expect, it, beforeEach } from "vitest"; +import { findSavingsBy, addSavingAccount } from "../src/queries/saving.ts"; +import type { Pool, PoolConnection } from "mysql2/promise"; +import { SavingAccountType } from "../src/types/SavingAccountType.ts"; +import { FinancialAccountType } from "../src/types/FinancialAccountType.ts"; + +describe("findSavingsBy", () => { + it("should return mapped saving accounts", async () => { + // Setup fake data + const mockRows = [ + { + id: "1", + name: "Savings A", + balance: 1000, + subtype: SavingAccountType.TFSA, + last_updated: "2022-10-17T00:00:00.000Z", + }, + { + id: "2", + name: "Savings B", + balance: 9000, + subtype: SavingAccountType.RRSP, + last_updated: "2026-02-08T19:00:00.000Z", + }, + ]; + + const mockDb = { + execute: vi.fn().mockResolvedValue([mockRows]), + } as unknown as Pool; + + const result = await findSavingsBy(mockDb, "30"); + + // Assert + expect(mockDb.execute).toHaveBeenCalled(); + + expect(result.length).toEqual(2) + expect(result[0]).toEqual( + { + id: 1, + name: "Savings A", + balance: 1000, + type: FinancialAccountType.SAVINGS, + subtype: SavingAccountType.TFSA, + lastUpdated: new Date("2022-10-17T00:00:00.000Z").toISOString(), + }, + ); + expect(result[1]).toEqual( + { + id: 2, + name: "Savings B", + balance: 9000, + type: FinancialAccountType.SAVINGS, + subtype: SavingAccountType.RRSP, + lastUpdated: new Date("2026-02-08T19:00:00.000Z").toISOString(), + }, + ); + }); + + it("should return empty array if no rows", async () => { + const mockDb = { + execute: vi.fn().mockResolvedValue([[]]), + } as unknown as Pool; + + const result = await findSavingsBy(mockDb, "20"); + + expect(result).toEqual([]); + }); + + it("should handle null/invalid last_updated safely", async () => { + const mockRows = [ + { + id: "3", + name: "Savings C", + balance: 500, + type: FinancialAccountType.SAVINGS, + subtype: SavingAccountType.RESP, + last_updated: null, + }, + ]; + + const mockDb = { + execute: vi.fn().mockResolvedValue([mockRows]), + } as unknown as Pool; + + const result = await findSavingsBy(mockDb, "50"); + + expect(result[0]?.lastUpdated).toBe("N/A"); + }); + + it("should call DB with correct query and userId", async () => { + const mockDb = { + execute: vi.fn().mockResolvedValue([[]]), + } as unknown as Pool; + + await findSavingsBy(mockDb, "1"); + + expect(mockDb.execute).toHaveBeenCalledWith( + expect.stringContaining("SELECT"), + ["1"] + ); + }); +}); + +describe("addSavingAccount", () => { + let mockConnection: Partial; + let mockDb: Partial; + + beforeEach(() => { + mockConnection = { + beginTransaction: vi.fn(), + commit: vi.fn(), + rollback: vi.fn(), + release: vi.fn(), + execute: vi.fn(), + query: vi.fn(), + }; + + mockDb = { + getConnection: vi.fn().mockResolvedValue(mockConnection), + }; + }); + + const newSavings = { + name: "Test Saving", + type: FinancialAccountType.SAVINGS, + balance: 1000, + value: 1000, + subtype: SavingAccountType.RRSP, + }; + + const userId = "10" + + it("should insert saving account and link profiles", async () => { + (mockConnection.execute as any) + // INSERT financialAccount + .mockResolvedValueOnce([{ insertId: 10 }]) + // SELECT profiles + .mockResolvedValueOnce([[{ id: 1 }, { id: 2 }]]); + + (mockConnection.query as any).mockResolvedValueOnce([]); + + const result = await addSavingAccount( + mockDb as Pool, + newSavings, + userId + ); + + // Assert + expect(mockConnection.beginTransaction).toHaveBeenCalled(); + expect(mockConnection.execute).toHaveBeenCalledTimes(2); + expect(mockConnection.query).toHaveBeenCalled(); // linking table + expect(mockConnection.commit).toHaveBeenCalled(); + expect(mockConnection.release).toHaveBeenCalled(); + + expect(result.id).toBe(10); + expect(result.name).toBe("Test Saving"); + expect(result.balance).toBe(1000); + }); + + // Should not insert into link table due to no profile found + it("should skip linking if no profiles found", async () => { + (mockConnection.execute as any) + .mockResolvedValueOnce([{ insertId: 20 }]) + .mockResolvedValueOnce([[]]); // no profiles + + const result = await addSavingAccount( + mockDb as Pool, + newSavings, + userId + ); + + expect(mockConnection.query).not.toHaveBeenCalled(); + expect(mockConnection.commit).toHaveBeenCalled(); + expect(result.id).toBe(20); + }); + + // Should rollback on error + it("should rollback if any query fails", async () => { + (mockConnection.execute as any) + .mockResolvedValueOnce([{ insertId: 30 }]) + .mockRejectedValueOnce(new Error("DB error")); // fail on profile query + + await expect( + addSavingAccount(mockDb as Pool, newSavings, userId) + ).rejects.toThrow("DB error"); + + expect(mockConnection.rollback).toHaveBeenCalled(); + expect(mockConnection.commit).not.toHaveBeenCalled(); + expect(mockConnection.release).toHaveBeenCalled(); + }); + + // Should call queries with correct params + it("should call INSERT with correct values", async () => { + (mockConnection.execute as any) + .mockResolvedValueOnce([{ insertId: 40 }]) + .mockResolvedValueOnce([[]]); + + await addSavingAccount(mockDb as Pool, newSavings, "user-1"); + + expect(mockConnection.execute).toHaveBeenCalledWith( + expect.stringContaining("INSERT INTO finus.financialAccount"), + [ + newSavings.name, + newSavings.type, + newSavings.balance, + newSavings.value, + newSavings.subtype, + ] + ); + }); +}); \ No newline at end of file diff --git a/documentation/sequence-diagrams/user-projection/user-flow-diagram.png b/documentation/sequence-diagrams/user-projection/user-flow-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..e40ca923a3ea48ade132f3ac72266c64cfb16618 GIT binary patch literal 72305 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

eFPSdCF9Wh531E=s;M>@CsmNdVN9_9T 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 Date: Fri, 27 Mar 2026 23:15:01 -0500 Subject: [PATCH 190/204] Feature user projection merge (#94) * Server: implemented debt and saving endpoints * More * Server: commented out new endpoints in auth service * Server: implemented debt payoff calculation but it's not working properly * Adjusted to using connection pool from db.ts to be used for sql queries and move some functions to their own file. * Created page for projection * Setup the graph for projection page and API calls for debt * Server: Fixed endpoints for fetching and creating user debts * Added handling of when there's is not data for the graph * Server: Fixed endpoints for fetching and creating saving account * Server: Finished predicted debt payoff calculation * Server: Implemented compound interest logic * Server: Implemented fetching historical transactions of saving financial account in user service * Setup the rest of the projected page, finished the api calls and fix some bugs * Server: Implemented unit tests for saving queries * Server: More unit tests * Projection graph for debt is workings * Typo fix * Server: finished compound interest computing and did some cleanups * Made so the graph can handle mutliples lines and handles getting saving projection response * Server: modified compound interest logic * Server: Added more unit tests * Saving projection graph works, also adjusted how forms look * Server: added user flow diagram * Fixed issues with quries - previous queries treated userID as profileID which steps over the user to profile to financial account hierarchy in schema * bug fix * bug fix * bug fix - reverted schema to old vairant for fin account type * temoporarily disabling integration tests due to a db error * disablign integration tests that rely on db * bug fix * fixed interest displaying a decimal after switching types and some bugs from reverting the schema. * Fixed interest * Fixed bugs due to the merge * Select accounts bug fix * Move handlenumberchange function to handleInput and added tests --------- Co-authored-by: Huy Truong Co-authored-by: Jackie Mei Co-authored-by: TebelR --- app/client/src/components/AccountForm.tsx | 7 +- app/client/src/components/AccountListCard.tsx | 5 +- app/client/src/components/SelectAccount.tsx | 9 ++- app/client/src/enum/AccountCategory.ts | 2 +- app/client/src/pages/ProjectionPage.tsx | 42 +++--------- app/client/src/utils/handleInput.ts | 31 +++++++++ app/client/test/handleInput.test.ts | 66 +++++++++++++++++++ app/server/services/ts/user/src/index.ts | 1 + .../services/ts/user/src/queries/debt.ts | 2 +- .../services/ts/user/src/queries/saving.ts | 2 +- .../ts/user/src/types/FinancialAccountType.ts | 10 +-- 11 files changed, 129 insertions(+), 48 deletions(-) diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx index 02c6c75..7ee8513 100644 --- a/app/client/src/components/AccountForm.tsx +++ b/app/client/src/components/AccountForm.tsx @@ -203,7 +203,7 @@ export default function PopupForm({ name="type" className="formSelect" onChange={(event) => - setAccountType(event.target.value as typeofAccount) + {setAccountType(event.target.value as typeofAccount); setFormInput({ ...formInput, ["subType"]: "" })} } > @@ -229,7 +229,7 @@ export default function PopupForm({ />

- {accountType === accountCategory.SAVING ? ( + {accountType === "SAVINGS" ? ( <>

- {account.type + " " + (account.subtype ? " " + account.subtype : "")} + {accountCategory[account.type as keyof typeof accountCategory] + + " " + + (account.subtype ? " " + account.subtype : "")}

{Number(account.balance).toFixed(2)}

diff --git a/app/client/src/components/SelectAccount.tsx b/app/client/src/components/SelectAccount.tsx index f2efbbf..b542d4a 100644 --- a/app/client/src/components/SelectAccount.tsx +++ b/app/client/src/components/SelectAccount.tsx @@ -1,5 +1,6 @@ import type { Account } from "@/types/AccountType"; import React from "react"; +import { accountCategory } from "@/enum/AccountCategory.ts"; interface selectProp { accounts: Account[]; @@ -26,7 +27,13 @@ export default function SelectAccount({ {accounts && accounts.map((account) => ( ))} diff --git a/app/client/src/enum/AccountCategory.ts b/app/client/src/enum/AccountCategory.ts index b35f4ec..33c4cc2 100644 --- a/app/client/src/enum/AccountCategory.ts +++ b/app/client/src/enum/AccountCategory.ts @@ -1,5 +1,5 @@ export const accountCategory = { - SAVING: "Savings", + SAVINGS: "Savings", CHEQUING: "Chequing", INVESTMENT: "Investment", CREDIT_CARD:"Credit Card", diff --git a/app/client/src/pages/ProjectionPage.tsx b/app/client/src/pages/ProjectionPage.tsx index b520687..1a63e8e 100644 --- a/app/client/src/pages/ProjectionPage.tsx +++ b/app/client/src/pages/ProjectionPage.tsx @@ -24,7 +24,11 @@ import { import ProjectionGraph from "@/components/ProjectionGraph"; import { TbGraph } from "react-icons/tb"; import NoItemState from "@/components/NoItemState"; -import { handleCurrencyChange, handleCurrencyBlur } from "@/utils/handleInput"; +import { + handleCurrencyChange, + handleCurrencyBlur, + handleNumberChange, +} from "@/utils/handleInput"; import type { projectionDebtRequest, projectionSavingRequest, @@ -106,7 +110,7 @@ function ProjectionPage({ session }: ProjectionProp) { category: accountCategory.CREDIT_CARD, remainingAmount: inputAmount, minimumPayment: inputMinPay, - interestRate: inputInterest / 100, + interestRate: inputInterest, nextDueDate: nextDueDate, period: inputPeriod, }; @@ -163,7 +167,7 @@ function ProjectionPage({ session }: ProjectionProp) { financial_account_id: selectedAccount, balance: inputAmount, monthly_deposit: inputMinPay, - annual_interest_rate: inputInterest / 100, + annual_interest_rate: inputInterest, time_frame: inputPeriod, }; @@ -233,7 +237,7 @@ function ProjectionPage({ session }: ProjectionProp) { if (savingRequest) { //Assigns the fields to what the projection of the saving account used setSelectedAccount(savingRequest.financial_account_id); - setAmount(savingRequest.balance.toFixed(2)); + setAmount((savingRequest.balance * 100).toFixed(2)); setInterest(savingRequest.annual_interest_rate.toString()); setMinPay(savingRequest.monthly_deposit.toFixed(2)); setPeriod(savingRequest.time_frame.toString()); @@ -270,36 +274,6 @@ function ProjectionPage({ session }: ProjectionProp) { } }; - //Handles on change of percentage - const handleNumberChange = ( - event: React.ChangeEvent, - setNumber: React.Dispatch>, - min: number, - max: number, - ) => { - let input = event.target.value; - let changeInterest; - const pattern = /^\d*\.?\d{0,2}$/; - - console.log(input); - console.log(pattern.test(input)); - - //Determine if the input follows the format/pattern - if (pattern.test(input) || input === "") { - input = input.replace(/^0+(?=\d)/, ""); - changeInterest = Number(input); - - if (changeInterest > max) { - changeInterest = max; - } - - if (changeInterest < min) { - changeInterest = min; - } - console.log(changeInterest); - setNumber(changeInterest.toString()); - } - }; //Get the saving accounts useEffect(() => { if (selectedType === "Saving") { diff --git a/app/client/src/utils/handleInput.ts b/app/client/src/utils/handleInput.ts index 7d05c0b..0b0a071 100644 --- a/app/client/src/utils/handleInput.ts +++ b/app/client/src/utils/handleInput.ts @@ -26,3 +26,34 @@ export const handleCurrencyBlur = ( setCurrency(parseFloat(currency).toFixed(2)); } }; + +//Handles on change of whole numbers +export const handleNumberChange = ( + event: React.ChangeEvent, + setNumber: React.Dispatch>, + min: number, + max: number, +) => { + let input = event.target.value; + let changeNumber; + const pattern = /^\d*\.?\d{0,2}$/; + + console.log(input); + console.log(pattern.test(input)); + + //Determine if the input follows the format/pattern + if (pattern.test(input) || input === "") { + input = input.replace(/^0+(?=\d)/, ""); + changeNumber = Number(input); + + if (changeNumber > max) { + changeNumber = max; + } + + if (changeNumber < min) { + changeNumber = min; + } + console.log(changeNumber); + setNumber(changeNumber.toString()); + } +}; diff --git a/app/client/test/handleInput.test.ts b/app/client/test/handleInput.test.ts index 5a59393..9fe8da4 100644 --- a/app/client/test/handleInput.test.ts +++ b/app/client/test/handleInput.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from "vitest"; import { handleCurrencyChange, handleCurrencyBlur, + handleNumberChange, } from "../src/utils/handleInput"; //tests for handleCurrencyChange and handleCurrencyBlur functions function mockEvent(value: string) { @@ -135,3 +136,68 @@ it("does not strip zeros inside the number", () => { expect(setCurrency).toHaveBeenCalledWith("101"); }); + +describe("handleNumberChange", () => { + it("accepts valid number input", () => { + const setNumber = vi.fn(); + const event = mockEvent("12"); + + handleNumberChange(event, setNumber, 0, 100); + + expect(setNumber).toHaveBeenCalledWith("12"); + }); + + it("Doesn't accept decimals", () => { + const setNumber = vi.fn(); + const event = mockEvent("12."); + + handleNumberChange(event, setNumber, 0, 100); + + expect(setNumber).not.toHaveBeenCalledWith(); + }); + + it("Empty string", () => { + const setNumber = vi.fn(); + const event = mockEvent(""); + + handleNumberChange(event, setNumber, 0, 100); + + expect(setNumber).toHaveBeenCalledWith(""); + }); + + it("strip leading 0s", () => { + const setNumber = vi.fn(); + const event = mockEvent("0000123"); + + handleNumberChange(event, setNumber, 0, 100); + + expect(setNumber).toHaveBeenCalledWith("123"); + }); + + it("rejects letters in input", () => { + const setNumber = vi.fn(); + const event = mockEvent("12A"); + + handleNumberChange(event, setNumber, 0, 100); + + expect(setNumber).not.toHaveBeenCalledWith(); + }); + + it("Sets the value to the maximum", () => { + const setNumber = vi.fn(); + const event = mockEvent("120"); + + handleNumberChange(event, setNumber, 0, 100); + + expect(setNumber).not.toHaveBeenCalledWith("100"); + }); + + it("Sets value to the minimum", () => { + const setNumber = vi.fn(); + const event = mockEvent("1"); + + handleNumberChange(event, setNumber, 10, 100); + + expect(setNumber).not.toHaveBeenCalledWith("10"); + }); +}); diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index 5017b71..451824c 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -58,6 +58,7 @@ process.on("SIGTERM", () => cleanup); app.get("/charts/expenses", async (req, res) => { process.on("SIGTERM", () => server.close()); +}) app.get( "/charts/expenses", diff --git a/app/server/services/ts/user/src/queries/debt.ts b/app/server/services/ts/user/src/queries/debt.ts index e9fb460..f331795 100644 --- a/app/server/services/ts/user/src/queries/debt.ts +++ b/app/server/services/ts/user/src/queries/debt.ts @@ -13,7 +13,7 @@ export async function findDebtsBy(db: Pool, userId: string) : Promise Date: Tue, 31 Mar 2026 20:54:21 -0500 Subject: [PATCH 191/204] Fix/integration test runner (#96) * removed node modules in server directory * re-worked how tests are run * modification to formatting hook * removed node modules in server directory * updated workflow runner * fixed tests, removed duplicate test file * cleanup test logging * commit to test pipeline * remove server reset key & change run test action targets * made run_tests.sh executable --- .github/workflows/run_tests.yml | 34 +-- app/client/src/App.tsx | 36 +-- app/client/src/utils/marketStorage.ts | 30 --- app/server/.env | 2 + app/server/docker-compose.test.yml | 60 ++--- app/server/docker-compose.yml | 2 +- .../results.json | 1 - app/server/run_tests.sh | 73 ++++++ .../python/analytics/src/dependencies.py | 7 +- .../services/python/analytics/src/main.py | 75 +++--- .../python/analytics/test/dependencies.py | 76 ------ .../analytics/test/test_dependencies.py | 5 +- .../integration/test_market_integration.py | 4 +- app/server/services/python/market/src/main.py | 3 +- .../services/python/market/test/test_main.py | 2 +- .../services/ts/api-gateway/src/index.ts | 236 ++++-------------- app/server/services/ts/auth/bun.lockb | Bin 6418 -> 39919 bytes app/server/services/ts/auth/src/index.ts | 7 +- app/server/services/ts/bun.lockb | Bin 119304 -> 161285 bytes app/server/services/ts/common/sqlUtil.ts | 33 ++- app/server/services/ts/common/types.ts | 2 + .../services/ts/user/integration/setup.ts | 2 +- .../services/ts/user/src/DataConversion.ts | 1 - app/server/services/ts/user/src/index.ts | 28 +-- .../services/ts/user/src/logic/goals.ts | 2 +- hooks/format_ts_dir.sh | 5 +- 26 files changed, 258 insertions(+), 468 deletions(-) delete mode 100644 app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json create mode 100755 app/server/run_tests.sh delete mode 100644 app/server/services/python/analytics/test/dependencies.py diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 1a6d321..93a0fb6 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [ "*" ] push: - branches: [ "main", "development", "release/*", "feature-stock-tracking" ] + branches: [ "main", "development", "release/*" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -16,35 +16,5 @@ jobs: working-directory: app/server steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - name: Build base images - run: docker compose build - - name: Build test images - run: | - docker compose -f docker-compose.yml -f docker-compose.test.yml build - - name: Start backend services - run: docker compose -f docker-compose.yml up -d database auth user analytics market api-gateway - name: Run tests - run: | - failed=() - for service in \ - auth-test \ - user-test \ - analytics-test \ - market-test \ - market-mutation-test \ - market-integration-test - do - echo "::group::Running $service" - if ! docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm "$service"; then - failed+=("$service") - fi - echo "::endgroup::" - done - - if [ ${#failed[@]} -gt 0 ]; then - echo "Failed test services: ${failed[*]}" - exit 1 - fi - - name: Stop services - if: always() - run: docker compose -f docker-compose.yml -f docker-compose.test.yml down -v --remove-orphans + run: ./run_tests.sh diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 391861b..d1ef33e 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { BrowserRouter as Router, Navigate, @@ -13,11 +13,6 @@ import DashboardPage from "./pages/DashboardPage.tsx"; import MarketsPage from "./pages/MarketsPage"; import AppLayout from "./components/AppLayout.tsx"; import ProjectionPage from "./pages/ProjectionPage.tsx"; -import { syncPinnedMarketsResetKey } from "./utils/marketStorage"; -//import { loadSession, saveSession, clearSession } from "./utils/storage.ts"; -//import { requestAuth } from "./api/AuthAPI"; -//import { resolveUserFromToken } from "./utils/token"; -// import type { AuthApiResponse, AuthSession, AuthUser } from "./pages/authTypes"; const SESSION_STORAGE_KEY = "finus-session"; const API_BASE_URL = @@ -142,35 +137,6 @@ function App() { loadSession(), ); - useEffect(() => { - let cancelled = false; - - async function syncServerResetKey() { - try { - const response = await fetch(`${API_BASE_URL}/client-state/reset-key`); - const data = (await response.json().catch(() => null)) as { - resetKey?: unknown; - } | null; - - if (cancelled || !response.ok) { - return; - } - - if (typeof data?.resetKey === "string" && data.resetKey.length > 0) { - syncPinnedMarketsResetKey(data.resetKey); - } - } catch { - // Ignore reset-key sync failures and keep the client usable offline. - } - } - - void syncServerResetKey(); - - return () => { - cancelled = true; - }; - }, []); - function handleAuthSuccess(token: string, fallbackUser: Partial) { const nextSession: AuthSession = { token, diff --git a/app/client/src/utils/marketStorage.ts b/app/client/src/utils/marketStorage.ts index 36a8e74..ecd94f5 100644 --- a/app/client/src/utils/marketStorage.ts +++ b/app/client/src/utils/marketStorage.ts @@ -3,7 +3,6 @@ import { loadSession } from "@/utils/storage"; const PINNED_MARKETS_STORAGE_KEY_PREFIX = "finus-pinned-markets"; const LEGACY_PINNED_MARKETS_STORAGE_KEY = PINNED_MARKETS_STORAGE_KEY_PREFIX; -const SERVER_RESET_KEY_STORAGE_KEY = "finus-server-reset-key"; const PINNED_MARKETS_CLEARED_EVENT = "finus:pinned-markets-cleared"; function resolvePinnedMarketsStorageKey(): string | null { @@ -26,21 +25,6 @@ function clearLegacyPinnedMarkets() { localStorage.removeItem(LEGACY_PINNED_MARKETS_STORAGE_KEY); } -function clearAllPinnedMarkets() { - clearLegacyPinnedMarkets(); - - for (let index = localStorage.length - 1; index >= 0; index -= 1) { - const key = localStorage.key(index); - if (!key) { - continue; - } - - if (key.startsWith(`${PINNED_MARKETS_STORAGE_KEY_PREFIX}:`)) { - localStorage.removeItem(key); - } - } -} - function loadPinnedMarkets(): PinnedMarketInstrument[] { clearLegacyPinnedMarkets(); @@ -84,23 +68,9 @@ function savePinnedMarkets(items: PinnedMarketInstrument[]) { localStorage.setItem(storageKey, JSON.stringify(items)); } -function syncPinnedMarketsResetKey(resetKey: string): boolean { - const previousResetKey = localStorage.getItem(SERVER_RESET_KEY_STORAGE_KEY); - if (previousResetKey === resetKey) { - return false; - } - - clearAllPinnedMarkets(); - localStorage.setItem(SERVER_RESET_KEY_STORAGE_KEY, resetKey); - window.dispatchEvent(new Event(PINNED_MARKETS_CLEARED_EVENT)); - return true; -} - export { - clearAllPinnedMarkets, loadPinnedMarkets, PINNED_MARKETS_CLEARED_EVENT, savePinnedMarkets, PINNED_MARKETS_STORAGE_KEY_PREFIX, - syncPinnedMarketsResetKey, }; diff --git a/app/server/.env b/app/server/.env index 314a0ae..a885f00 100644 --- a/app/server/.env +++ b/app/server/.env @@ -1,6 +1,8 @@ #THIS IS NOT .gitignored'D! #DON'T PUT REAL SECRETS IN HERE!!! +API_GATEWAY_PORT=3000 + #CI DOCKER_HUB_USERNAME=measureonecodetwice RELEASE_VERSION=local diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 76428b5..31b564a 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -1,9 +1,6 @@ -#This compose file runs all the tests. -#It depends on the main compose file being built -#as all the test images inherit from the regular images. -#you can use the following command to run the tests -#docker compose build -q && docker compose -f docker-compose.yml -f docker-compose.test.yml build -q && docker compose -f docker-compose.yml -f docker-compose.test.yml up --abort-on-container-failure +#Run with ./run-tests.sh services: + # ------ UNIT TESTS ------ auth-test: build: context: services/ts @@ -19,56 +16,39 @@ services: context: services/python/analytics dockerfile: Dockerfile.test - # user-integration-test:#uncomment to run integration tests locally as it likely does not work properly in CI/CD - # build: - # context: ./services/ts - # dockerfile: user/Dockerfile.integration.test - # env_file: - # - ./default_container.env - # environment: - # - JWT_SECRET - - # depends_on: - # database: - # condition: service_healthy - # api-gateway: - # condition: service_started + market-test: + build: + context: services/python/market + dockerfile: Dockerfile.test + # # ------ MUTATION TESTS ------ # user-mutation-test: # build: # context: ./services/ts # dockerfile: user/Dockerfile.mutation.test - market-test: - build: - context: services/python/market - dockerfile: Dockerfile.test market-mutation-test: build: context: services/python/market dockerfile: Dockerfile.mutation.test + # MISSING + # ANALYTICS + # AUTH + + # ------ INTEGRATION TESTS ------ market-integration-test: build: context: services/python/market dockerfile: Dockerfile.integration.test env_file: - ./default_container.env - depends_on: - market: - condition: service_started - # user-integration-test: - # build: - # context: ./services/ts - # dockerfile: user/Dockerfile.integration.test - # env_file: - # - ./default_container.env - # environment: - # - JWT_SECRET - - # depends_on: - # database: - # condition: service_healthy - # api-gateway: - # condition: service_started + user-integration-test: + build: + context: ./services/ts + dockerfile: user/Dockerfile.integration.test + env_file: + - ./default_container.env + environment: + - JWT_SECRET diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index 906ab34..c5d1445 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -9,7 +9,7 @@ services: environment: - NODE_ENV=development ports: - - "3000:8000" + - "$API_GATEWAY_PORT:8000" volumes: - client_state_data:/var/lib/finus-client-state depends_on: diff --git a/app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json deleted file mode 100644 index c418db7..0000000 --- a/app/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"4.1.1","results":[[":services/ts/user/test/logic_snapshot.test.ts",{"duration":24.460399999999993,"failed":true}],[":services/ts/user/test/dates.test.ts",{"duration":28.86179999999996,"failed":true}],[":services/ts/user/test/logic_expenses.test.ts",{"duration":14.341799999999978,"failed":false}],[":services/ts/auth/test/logic.test.ts",{"duration":0,"failed":true}],[":services/ts/user/test/logic_transactions.test.ts",{"duration":8.949200000000019,"failed":false}],[":services/ts/auth/test/parsing.test.ts",{"duration":0,"failed":true}],[":services/ts/auth/test/validation.test.ts",{"duration":6.108399999999989,"failed":false}]]} \ No newline at end of file diff --git a/app/server/run_tests.sh b/app/server/run_tests.sh new file mode 100755 index 0000000..ea4ddcc --- /dev/null +++ b/app/server/run_tests.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +quitting=false +quit() { + if ! $quitting; then + quitting=true + cleanup + exit 0 + fi +} +cleanup() { + echo "Cleaning up..." + docker compose -f docker-compose.test.yml -f docker-compose.yml down +} +trap quit SIGINT + +#--- Build images --- +echo "Building app images..." +docker compose build -q +status=$? +if [ $status -ne 0 ]; then + echo "Failed to build services" + exit $status +fi + +echo "Building test images..." +docker compose -f docker-compose.test.yml build -q +status=$? +if [ $status -ne 0 ]; then + echo "Failed to build test images" + exit $status +fi + +#--- Launch services --- +docker compose up -d + +#--- Wait for services to be healthy --- +source .env + +is_app_healthy() { + echo $(curl localhost:$API_GATEWAY_PORT/health 2> /dev/null | grep -Pc '^{(\"[^\"]*\":\"ok\",?)*}$') +} + +curr_checks=0 +max_checks=10 +while [[ healthy=$(is_app_healthy) -ne 1 ]] && [ $curr_checks -lt $max_checks ]; do + curr_checks=$(($curr_checks + 1)) + sleep 2.5 + echo "App unhealthy. Retrying ($curr_checks of $max_checks)" +done + +if [ $healthy -ne 1 ]; then + echo "App unhealthy after $max_checks retries. Exiting" + exit 1 +fi + +#--- Run tests --- +docker compose -f docker-compose.test.yml up --abort-on-container-failure +test_status=$? + +if [ $test_status -eq 0 ]; then + echo '+--------------------+' + echo '| ✅ All tests pass! |' + echo '+--------------------+' +else + echo '+--------------------+' + echo '| ❌ Tests failed |' + echo '+--------------------+' + +fi +cleanup + +exit $test_status diff --git a/app/server/services/python/analytics/src/dependencies.py b/app/server/services/python/analytics/src/dependencies.py index ebb1ac0..e64f222 100644 --- a/app/server/services/python/analytics/src/dependencies.py +++ b/app/server/services/python/analytics/src/dependencies.py @@ -9,7 +9,7 @@ SECRET_KEY = os.getenv('JWT_SECRET') ALGORITHM = 'HS256' -def get_db_connection(): +def get_db_connection(timeout: int = 10): #check env vars if not all([os.getenv("MYSQL_HOST"), os.getenv("MYSQL_USER"), os.getenv("MYSQL_PASSWORD"), os.getenv("DB_NAME")]): raise HTTPException(status_code=500, detail="Missing environment variables") @@ -17,7 +17,8 @@ def get_db_connection(): host=os.getenv("MYSQL_HOST"), user=os.getenv("MYSQL_USER"), password=os.getenv("MYSQL_PASSWORD"), - database=os.getenv("DB_NAME") + database=os.getenv("DB_NAME"), + connection_timeout=timeout ) async def get_current_user(authorization: Optional[str] = Header(None)): @@ -35,4 +36,4 @@ async def get_current_user(authorization: Optional[str] = Header(None)): return int(user_id) except Exception as e: - raise HTTPException(status_code=401, detail=str(e))#general exception for simpler unit \ No newline at end of file + raise HTTPException(status_code=401, detail=str(e))#general exception for simpler unit diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py index eb80e9b..c0ada55 100644 --- a/app/server/services/python/analytics/src/main.py +++ b/app/server/services/python/analytics/src/main.py @@ -1,7 +1,8 @@ from fastapi import FastAPI, Depends, Query, HTTPException +from fastapi.responses import PlainTextResponse from fastapi.middleware.cors import CORSMiddleware import pandas as pd -from typing import List, Dict +from typing import List import uvicorn import os @@ -11,7 +12,12 @@ from src.logic.budget import generate_budget, generate_budget_performance from src.queries import savings as savings_queries, incomeflow as incomeflow_queries from src.logic import savings as savings_service, incomeflow as incomeflow_service, debt as debt_service -from src.models.schemas import BadRequestError, ProjectedSavingsRequest, ProjectedSavingsResponse, CompoundInterestResponse, DebtPayoffRequest +from src.models.schemas import ( + BadRequestError, + ProjectedSavingsRequest, + CompoundInterestResponse, + DebtPayoffRequest +) app = FastAPI() @@ -27,50 +33,48 @@ ) - @app.get('/charts/savings') async def get_savings(period: str, user_id: int = Depends(get_current_user)): try: connection = get_db_connection() if not connection: raise HTTPException(status_code=500, detail="Database connection failed") - + cursor = connection.cursor(dictionary=True) - + # Get savings accounts accounts = savings_queries.get_savings_accounts(cursor, user_id) if not accounts: cursor.close() connection.close() return None - + # Get transactions account_ids = [acc['id'] for acc in accounts] transactions = savings_queries.get_savings_transactions(cursor, account_ids) - + cursor.close() connection.close() - + if not transactions: return None - + # Calculate date range end_date = pd.Timestamp.today() - start_date, freq, date_format, period_name = period_calc(period, end_date) - + start_date, _, _, period_name = period_calc(period, end_date) + # Process data result = savings_service.calculate_savings_over_time( accounts, transactions, start_date, end_date, period, period_name ) - + return result - + except Exception as e: print(f"Error: {e}") raise HTTPException(status_code=500, detail=f"Error: {e}") - @app.get('/charts/incomeflow') async def get_income_flow( period: str = Query(default='w', enum=['w', 'm', 'y']), @@ -80,31 +84,30 @@ async def get_income_flow( connection = get_db_connection() if not connection: raise HTTPException(status_code=500, detail="Database connection failed") - + cursor = connection.cursor(dictionary=True) - + # Calculate date range end_date = pd.Timestamp.today() start_date, _, _, _ = period_calc(period, end_date) - + # Get transactions transactions = incomeflow_queries.get_user_transactions(cursor, user_id, start_date, end_date) - + cursor.close() connection.close() - + if not transactions: return None - + # Build sankey data return incomeflow_service.build_sankey_data(transactions) - + except Exception as e: print(f"Error generating income flow: {str(e)}") raise HTTPException(status_code=500, detail=f"Error generating income flow: {str(e)}") - @app.get('/charts/budget-expenditure') async def get_budget( period: str = Query(default='w', enum=['w', 'm', 'y']), @@ -119,37 +122,49 @@ async def get_budget( except Exception as e: print(e) raise HTTPException(status_code=500, detail=str(e)) - + + @app.post('/predict-debt-payoff') async def predict_debt_payoff( requestBody: DebtPayoffRequest, user_id: int = Depends(get_current_user), ): try: - print(f"Generating predicted debt payoff for user {user_id}") - print(requestBody) - return debt_service.generate_debt_payoff_stages(requestBody) + print(f"Generating predicted debt payoff for user {user_id}") + print(requestBody) + return debt_service.generate_debt_payoff_stages(requestBody) except BadRequestError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: print(e) raise HTTPException(status_code=500, detail=str(e)) + @app.post('/compound-interest') async def compount_interest( requestBody: ProjectedSavingsRequest, user_id: int = Depends(get_current_user), ) -> List[CompoundInterestResponse]: try: - print(f"Generating compound interests for user {user_id}") - print(requestBody) - return savings_service.calculate_compound_interest(requestBody) + print(f"Generating compound interests for user {user_id}") + print(requestBody) + return savings_service.calculate_compound_interest(requestBody) except Exception as e: print(e) raise HTTPException(status_code=500, detail=str(e)) +@app.get('/health', response_class=PlainTextResponse) +async def health(): + status = 'ok' + try: + conn = get_db_connection(1) + conn.close() + except Exception: + status = 'unhealthy' + + return status if __name__ == "__main__": port = int(os.getenv("PORT", 8000)) - uvicorn.run(app, port=port) \ No newline at end of file + uvicorn.run(app, port=port) diff --git a/app/server/services/python/analytics/test/dependencies.py b/app/server/services/python/analytics/test/dependencies.py deleted file mode 100644 index df09d1d..0000000 --- a/app/server/services/python/analytics/test/dependencies.py +++ /dev/null @@ -1,76 +0,0 @@ -import pytest -from unittest.mock import patch, MagicMock -import mysql.connector -from src.dependencies import get_db_connection, get_current_user -from fastapi import HTTPException -import os -import jwt -from jwt import InvalidSignatureError - -class TestDatabaseConnection: - - @patch('mysql.connector.connect') - def test_get_db_connection_success(self, mock_connect, mock_env_vars): - mock_connection = MagicMock() - mock_connect.return_value = mock_connection - - result = get_db_connection() - - mock_connect.assert_called_once_with( - host='localhost', - user='test_user', - password='test_password', - database='test_db' - ) - - assert result == mock_connection - - @patch('mysql.connector.connect') - def test_get_db_connection_missing_env_vars(self, mock_connect): - with patch.dict(os.environ, {}, clear=True): - with pytest.raises(HTTPException): - get_db_connection() - - @patch('mysql.connector.connect') - def test_get_db_connection_failure(self, mock_connect): - mock_connect.side_effect = mysql.connector.Error("Connection failed") - with patch.dict(os.environ, {'MYSQL_HOST': 'localhost', 'MYSQL_USER': 'test_user', 'MYSQL_PASSWORD': 'test_password', 'DB_NAME': 'test_db'}, clear=True): - with pytest.raises(mysql.connector.Error): - get_db_connection() - - - -class TestJWTAuthentication: - - @pytest.mark.asyncio - async def test_get_current_user_missing_user_id(self): - - payload = {'email': 'test@example.com'} - token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256') - - # Ensure token is string - if isinstance(token, bytes): - token = token.decode('utf-8') - - with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): - with pytest.raises(HTTPException) as exc_info: - await get_current_user(authorization=f"Bearer {token}") - - assert exc_info.value.status_code == 401 - assert exc_info.value.detail == "User ID not found in token" - - @pytest.mark.asyncio - async def test_get_current_user_invalid_signature(self): - - payload = {'sub': '123'} - token = jwt.encode(payload, 'wrong_secret', algorithm='HS256') - - if isinstance(token, bytes): - token = token.decode('utf-8') - - with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}): - with pytest.raises(HTTPException) as exc_info: - await get_current_user(authorization=f"Bearer {token}") - - assert exc_info.value.status_code == 401 - assert "token" in exc_info.value.detail.lower() \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_dependencies.py b/app/server/services/python/analytics/test/test_dependencies.py index b148cde..7a32238 100644 --- a/app/server/services/python/analytics/test/test_dependencies.py +++ b/app/server/services/python/analytics/test/test_dependencies.py @@ -21,7 +21,8 @@ def test_get_db_connection_success(self, mock_connect, mock_env_vars): host='localhost', user='test_user', password='test_password', - database='test_db' + database='test_db', + connection_timeout=10 ) assert result == mock_connection @@ -123,4 +124,4 @@ async def test_get_current_user_no_bearer(self): await get_current_user(authorization=f'something {token}') assert exc_info.value.status_code == 401 - # assert exc_info.value.detail == '401: Invalid authentication scheme' \ No newline at end of file + # assert exc_info.value.detail == '401: Invalid authentication scheme' diff --git a/app/server/services/python/market/integration/test_market_integration.py b/app/server/services/python/market/integration/test_market_integration.py index a50b45c..de030b8 100644 --- a/app/server/services/python/market/integration/test_market_integration.py +++ b/app/server/services/python/market/integration/test_market_integration.py @@ -16,7 +16,7 @@ def wait_for_market_service(base_url: str, timeout_seconds: float) -> None: while time.monotonic() < deadline: try: response = httpx.get(f"{base_url}/health", timeout=5.0) - if response.status_code == 200 and response.json() == "ok": + if response.status_code == 200 and response.text == "ok": return last_error = f"unexpected health response: {response.status_code} {response.text}" except httpx.HTTPError as exc: @@ -39,7 +39,7 @@ def test_market_health_endpoint(market_client: httpx.Client) -> None: response = market_client.get("/health") assert response.status_code == 200 - assert response.json() == "ok" + assert response.text == "ok" def test_search_endpoint_returns_enriched_market_results( diff --git a/app/server/services/python/market/src/main.py b/app/server/services/python/market/src/main.py index 2a4c945..c19d8dc 100644 --- a/app/server/services/python/market/src/main.py +++ b/app/server/services/python/market/src/main.py @@ -2,6 +2,7 @@ import uvicorn from fastapi import FastAPI, HTTPException, Query +from fastapi.responses import PlainTextResponse from .config import DEFAULT_SEARCH_LIMIT, SUPPORTED_INTERVALS, SUPPORTED_PERIODS from .service import build_history, enrich_search_results, fetch_quote_snapshot @@ -14,7 +15,7 @@ app = FastAPI() -@app.get("/health") +@app.get('/health', response_class=PlainTextResponse) def test_endpoint(): return "ok" diff --git a/app/server/services/python/market/test/test_main.py b/app/server/services/python/market/test/test_main.py index e5bf976..51832a0 100644 --- a/app/server/services/python/market/test/test_main.py +++ b/app/server/services/python/market/test/test_main.py @@ -14,7 +14,7 @@ def test_health_endpoint(): response = client.get("/health") assert response.status_code == 200 - assert response.json() == "ok" + assert response.text == "ok" def test_search_route_combines_sources_and_enriches_results(): diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 0313249..5ba37c1 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -3,215 +3,79 @@ import { onExit } from "@/hooks"; import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; import { buildCorsConfig } from "@/expressUtils.ts"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { randomUUID } from "node:crypto"; const app = express(); app.use(buildCorsConfig()); -const CLIENT_STATE_DIR = - process.env.CLIENT_STATE_DIR ?? "/var/lib/finus-client-state"; -const CLIENT_STATE_RESET_KEY_PATH = path.join( - CLIENT_STATE_DIR, - "reset-key.txt", -); - -function resolveClientStateResetKey() { - try { - const existingKey = fs - .readFileSync(CLIENT_STATE_RESET_KEY_PATH, "utf8") - .trim(); - if (existingKey.length > 0) { - return existingKey; - } - } catch { - // Fall through and create a new reset key. - } - - const nextKey = randomUUID(); - fs.mkdirSync(CLIENT_STATE_DIR, { recursive: true }); - fs.writeFileSync(CLIENT_STATE_RESET_KEY_PATH, nextKey, "utf8"); - return nextKey; -} - -const clientStateResetKey = resolveClientStateResetKey(); - -const pathMatches = (path, valid) => { - path = path.replace("/api/", ""); +const pathMatches = (path, valid): boolean => { + path = path.replace("/api", ""); return valid.includes(path); }; -const AUTH_PATHS = ["signup", "login"]; -app.use( - createProxyMiddleware({ - pathFilter: (path) => pathMatches(path, AUTH_PATHS), - target: process.env.AUTH_SERVICE_ADDR, - changeOrigin: true, - pathRewrite: { "^/api": "" }, - }), -); +const registerProxy = (target: string, paths: string[]): void => { + app.use( + createProxyMiddleware({ + pathFilter: (path) => pathMatches(path, paths), + target: target, + changeOrigin: true, + pathRewrite: { "^/api": "" }, + }), + ); +}; + +registerProxy(process.env.AUTH_SERVICE_ADDR, ["/signup", "/login"]); -const USER_PATHS = [ - "accounts", - "profiles", +registerProxy(process.env.MARKET_SERVICE_ADDR, [ + "/markets/search", + "/markets/quote", + "/markets/history", +]); + +registerProxy(process.env.USER_SERVICE_ADDR, [ + "/accounts", + "/profiles", + "/goals", + "/debts", + "/transactions", "/charts/expenses", "/table/transactions", "/table/snapshot", - "/goals", -]; -app.use( - createProxyMiddleware({ - pathFilter: (path) => pathMatches(path, USER_PATHS), - target: process.env.USER_SERVICE_ADDR, - changeOrigin: true, - pathRewrite: { "^/api": "" }, - }), -); + "/transactions/csvTransaction", +]); -const MARKET_PATHS = ["markets/search", "markets/quote", "markets/history"]; -app.use( - createProxyMiddleware({ - pathFilter: (path) => pathMatches(path, MARKET_PATHS), - target: process.env.MARKET_SERVICE_ADDR, - changeOrigin: true, - pathRewrite: { "^/api": "" }, - }), -); - -app.use( - createProxyMiddleware({ - pathFilter: "/api/transactions", - target: process.env.USER_SERVICE_ADDR, - changeOrigin: true, - pathRewrite: { "^/api/transactions": "/transactions" }, - }), -); +registerProxy(process.env.ANALYTICS_SERVICE_ADDR, [ + "/charts/savings", + "/charts/incomeflow", + "/charts/budget-expenditure", + "/compound-interest", + "/predict-debt-payoff", +]); app.get("/health", async (req: express.Request, res: express.Response) => { - const result: { [key: string]: string } = {}; const services: string[] = Object.keys(process.env).filter((x) => /^.*_SERVICE_ADDR$/.test(x), ); - for (const service of services) { - const serviceName = service.split("_")[0]; - result[serviceName] = await fetch(`${process.env[service]}/health`) + //parallel arrays + const serviceNames: string[] = services.map( + (service) => service.split("_")[0], + ); + const serviceStatusPromises: [Promise] = services.map((service) => + fetch(`${process.env[service]}/health`) .then((res) => res.text()) - .catch((err) => err.message); - console.log("received response"); - } + .catch((err) => err.message), + ); - res.json(result); -}); + const serviceStatuses = await Promise.all(serviceStatusPromises); + const results = {}; + services.forEach( + (_, index) => (results[serviceNames[index]] = serviceStatuses[index]), + ); -app.get( - "/client-state/reset-key", - (_req: express.Request, res: express.Response) => { - res.json({ resetKey: clientStateResetKey }); - }, -); + res.json(results); +}); const server = app.listen(PORT, () => { console.log(`API Gateway running on port ${PORT}`); }); onExit(async () => await server.close()); - -//expenses bar chart in user service -// app.use( -// createProxyMiddleware({ -// pathFilter: ["/charts/expenses"], -// target: process.env.USER_SERVICE_ADDR, -// changeOrigin: true, -// }), -// ); - -//transactions table in user service -// app.use( -// createProxyMiddleware({ -// pathFilter: ["/table/transactions"], -// target: process.env.USER_SERVICE_ADDR, -// changeOrigin: true, -// }), -// ); - -//snapshot of total values like debt, savings, etc from user service -// app.use( -// createProxyMiddleware({ -// pathFilter: ["/table/snapshot"], -// target: process.env.USER_SERVICE_ADDR, -// changeOrigin: true, -// }), -// ); - -//savings chart from analytics service -app.use( - createProxyMiddleware({ - pathFilter: ["/charts/savings"], - target: process.env.ANALYTICS_SERVICE_ADDR, - changeOrigin: true, - }), -); - -//income flow chart from analytics service - this is the sankey chart -app.use( - createProxyMiddleware({ - pathFilter: ["/charts/incomeflow"], - target: process.env.ANALYTICS_SERVICE_ADDR, - changeOrigin: true, - }), -); - -//budget-expenditure chart from analytics service -app.use( - createProxyMiddleware({ - pathFilter: ["/charts/budget-expenditure"], - target: process.env.ANALYTICS_SERVICE_ADDR, - changeOrigin: true, - }), -); - -//compound interest from analytics service -app.use( - createProxyMiddleware({ - pathFilter: ["/compound-interest"], - target: process.env.ANALYTICS_SERVICE_ADDR, - changeOrigin: true, - }), -); - -//debt payoff prediction from analytics service -app.use( - createProxyMiddleware({ - pathFilter: ["/predict-debt-payoff"], - target: process.env.ANALYTICS_SERVICE_ADDR, - changeOrigin: true, - }), -); - -//debt-related request -app.use( - createProxyMiddleware({ - pathFilter: "/api/debts", - target: process.env.USER_SERVICE_ADDR, - changeOrigin: true, - pathRewrite: { "^/api/debts": "/debts" }, - }), -); -//savings-related request -app.use( - createProxyMiddleware({ - pathFilter: "/api/savings", - target: process.env.USER_SERVICE_ADDR, - changeOrigin: true, - pathRewrite: { "^/api/savings": "/savings" }, - }), -); -// market search, quote, and history endpoints from market service -app.use( - createProxyMiddleware({ - pathFilter: ["/markets/search", "/markets/quote", "/markets/history"], - target: process.env.MARKET_SERVICE_ADDR, - changeOrigin: true, - }), -); diff --git a/app/server/services/ts/auth/bun.lockb b/app/server/services/ts/auth/bun.lockb index c9d0fb033da37250057cce6ddf83c94391808f4e..55281ee90984df9b02f2f67b91d7adcc99acf8b6 100755 GIT binary patch literal 39919 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 PQJ%ul{=)PBzyJRazw&E; delta 1080 zcmcIj-%Ha`6hGg)t!>|ZzdD<=nTtw-GWEO7)$EsRPhm|>5}Ig*R#Tg819h2#HpE`c zL_K>LLBzm^K@yQl3pMCbq4$D}9wL+;ihe**51qSxXMaF+;Cw&#^SS5V^S$?+`{^m0 zsh(2G=M(n^7iyDj{>4;9`$VWGGne|tu1=i3@>*(YS)cegYJ;qqrvtlhHJNh+pd;4b zogYg8@mV6pgb6|lijS!nBP=A|sbWCIdO{QFeZlaJU@t&uppO&)t0t$120F4lMSLg4 zO%hrxGzjP7pzY3K$+42Yk528)8GRj-0LMiy%K)ws7z7keUSpF0P)Cb2I4#z&q=4fj zz9sS0mbj0hD0x|k;TeKOhU1b~x5)q$ylXnn$}}j-USm6@S7;5xX;Y1HiP!~V0%y#s z#WmT>I<+Vkc+jlZ;#9NtA|(NloDP|_e?j^ikkx%SYH-)y(jXUjmj9j!|?7i1iC2C$`4#&u$aSnfKDmWBoN(GC8tHyF}%Y&vZmiyG4NQ_b&~m{i+u&Wz`} z8Vjhe;FT&B7vZC>W0>r=Z!aL3Px*@ILpH{z${P7KahVszNNyn4lW!x}k!#7RuD~yw zIyzf8UNpWPzjQv%s9W}0@ceb9m^Z$mcDQS1_5BkjTso>a6*umQmEi424XX!y9d>el^w;Mt5l{2Pz} z^hIrK7O*v1&8!-nB$fkv_NCB|MXZh!KQrGwOmNYzE*C&bgKxIt#ASL-+?Fk9AMuGB jDjl+6=SZ0p>WfAOf|vWFJ;4aR7%QD08ST+hC6a#twA;@Z diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts index 147bc3f..1e664ad 100644 --- a/app/server/services/ts/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -5,7 +5,7 @@ import { buildCorsConfig, handleServerError } from "@/expressUtils"; import { signup, login } from "./logic"; import type { LoginBody, SignupBody } from "./types"; import { parseLoginBody, parseSignupBody } from "./parsing"; -import { getConnectionPool } from "@/sqlUtil"; +import { getConnectionPool, getDatabaseStatus } from "@/sqlUtil"; const pool = getConnectionPool(); const app = express(); @@ -37,8 +37,9 @@ app.post("/login", async (req, res) => { handleServerError(() => login(body, res, pool), res); }); -app.get("/health", (_, res) => { - res.send({ ok: true }); +app.get("/health", async (_, res) => { + const status = await getDatabaseStatus(); + res.send(status); }); const server = app.listen(PORT, () => { diff --git a/app/server/services/ts/bun.lockb b/app/server/services/ts/bun.lockb index 1e1e37c3b08461f7498747bc8ea4e2fa370dbd00..c41d30d8f400b0e1dcc82b6d6f394944bb76729b 100755 GIT binary patch literal 161285 zcmeFad0dTK8$Z0$pczrAXjDprC{i@ih%}fIX`biZtWpxnRE7pBnTgCQ3ejLpltM^U zN=ib?{JSoDujky)dCy^2@B90_e?04R?!J4i`}%&bYh7!&hrQ1^ax#-c>A{noz5FM; z`G-k6hx+rvq3Rpp;^Xe+>!#`+5bWm^q8g^oJB&u7MJ{aI;gu6;9WzOxBHemi{_-i! zV)y3Te>Qf1GW|1GK|yFJTG41b*?s|yn*sm9IA;B73e$jDr_naRCvh4r4xImLO9der zsM7%@0T%;~2h;%^2PnxzPCzlJcY)woz&a*g1{8(*Q9xlpjA#tt8o<$j6PfMo5aKAP zI{@+mhJ^b2yZO>+-gH$zS4|qt)s3#^O{dWec^Et|e^0kyuMm0=v|$+zB!1v+1VsH+ zfFl8y0}29u1{45P03(d&H6NpH3+*5oHwM~;0676i0Dgk=h>d{oPh1`RLA#Y8hJHK( z0$tIsrxQIO98%Q{I`B`NH~hhV^bKd&h0uMyoWbrb)REr;h<0>0zc9C88Z9^idqtxK z1_yWqyV2>7m^^n7!FU6mLOfM{+#)YRJLZ2MAm%p-5c3_xtVad>3WsI|TIi=7l8f<+ zK{BxaE?|ea1Q6qQi*R#s0^40MDA>RCfIx|hX4dI|s1xWEOm|cDqkA*!4uHtF2E_PG z0kJ=iA*mRbyO&2O^n(@#6�Ia31x?La-Q*A0V|~;htVDo)Cu@9U=mh0ecCsn+I_s zZx$eqqXrjQR9-eK?;#vLXh`uPYXjK38S{kS{PLsUb9y@Z(aO9c2hZ-XF- z7{3D`+W!Cz z_5#|me^r2}dleAvvj9;)1^C#HNGD%kFM0@#%!5v88Vy{<87k0dU>YY6i0kVhv}1o< zCNkE~IjH0O;#OqnJb-vHKL?nQ5IK!T zOJn+Rb#o5&fVv+Y{y=#s&G1_Ri2dIHi1Wz9&7U6W?*i*Hz<(OpjevG=6?Yywf%``n zAoim{jYgXacnT0)##yM-Xexjc0HKRh9EExGSeCHjReH@Va)nhKpfXDW_^Pe<9st9&bJn5$8q@x zI)Z>3nR-!xsN)HU{a?y#HwVOV(Pq|_n7C1gF%BOAaa~xZLq6b2yHCbp&l0? z>UsDEI79kq=VmeH(*Z#2M*wX~73f5#lX;T@?HH%E9%DTHBI!ZCkl#jA>P+j0 zI-UjU|Js*AgMG!B{sfu$l4%zb8R(|! zMW;6fZfRDV>HVnTsTLv~jJI0j;b<8i_%frjxJ%EOBqP{!bEzHe7gkAu4 zn6CqXXy@+g1zCaXSb(puD_jZpEn%$BcqY06!jg^aTFQvS!;TT3Js|d@6A<^wxq#SD zW3b0{Ee9wINCSi|Hm-3oqh1Oq3H3dIGJsKl@_;J<(T^^mG@v9P>ik&5sJ{dp2lXmI z%ttXGu8Y6`dWZ|mb9P5Y9{ipB1H52HTP$S6*X_jE&mIEeepCX8`^6qW^tT!i{kZ_5 zz6GEdpei7q7Xn24e$dB#@dcnLU_Bs?bBLFJBwma2nfauLxT?YpAT5Bg4bwJ5JKDtp z{bS1GX$(Iw48Q7-(_iE42KO)iFi#KpG3q@248Oi^VepAY`^jwYWa1}4oENTc zZh^S|(xHxV2Ls|bdC^sWZFwGn3_TBM$NqVF_y+{TOk2*ZyLy-bO~^EBljoL;f(R;WZJj+GUjmupaAe?BN=!D>Nq|nfc${1(2n&(P!|E*28jM@ zq8Q^E33Z%r_JFu9Nc;&tu(v?HFPgD#>H%e-z6TKJaUmeCkL`f`fG%J!4QK&~dSh2J z>R)3S^LQ$>Bae&|WGf`Z%PrUna)bH!`~9Z3SBMv!qIrhWeV_|89CRnh`yBX;{x+{+ z^aC!Fzw$pBkPq7b-v0#&j6D0f`m4G+g}`cJl<-{+zV$(#Yf9rAk5)C#eDMA8D*?96 z&oj@c&dw{Q?@~VeL!o83Se@yQf+wfy&%M2{$;d*uJ4^fd!3_J^H{uFhbn}W_?p`<3 zk!nfYS@l}lPN2Zn%uhv`+nHa~beWKP1E2k!&;tP`yRLg|Ioj4MtWoQ==;g7RDupW@ z&UD#(3VCeN^4C`=9PwOYs`qT-6N!b7US+=vucWo#TXCZ!WAEhr>P#P9&*U%bNA2nm z9sB%&hiF8CyI=N)DglxHW$!jM#k>}9kqdt8_)Vcdc!N2Y9hY&W(XQgF#sZxub@z@r zzerI=pl9D_uJIYWGjE9p`4#cW&zH$9*MB;C?4DJbn~yAtcaz6PkCb zjOI*MpOt-Up+n5k{X4V5mP{SLIok5p@(Pu9v6zXLHS{Y#>TG85I92N1-mpJ*SrG?q z&gu^*_kAlILHBdsX}!-lq`P#ry1VN1<~OEuR$rAG_R_)n?b|@RD{nco8oLyiSU;G* z;{DARYV$sCd1L-IZMdmT*opE`$rwAk@s1z2k5i7`w{QG1=ZS%fs+YDc30{5K#;a`D zkJ=Q$iT94YTpf^@JI+Wv#!aNB-qPgoo2@T2$4VO6%f>66KbV%&$tmq0+=CyU&**H__aSi8r!h;SyqlHZr^R$ZfEqQwS>~RC(=NURzxMO%^pEVu*lgxgc zJ2)Zwn{V0o`SuYj3&&PGy?5_N%bS@_PB*7o@@Q;xiMpDd8?#=$ZPM5+IpOchV*;}t z-l=_`eyUzPTIM~O2X;{hF7*mGo_J*y#Xf&gW}(C5cWRco;z{EDeqBfB$|lbYJtVP; zV_&bu`JYXV%bOOpA4wijU(Xfz#(UK|9-ZlXB74q1dZJ$v`+1a`&kxP2Ohn6FAF z*x5mM?6Gj`$4;eMccvYVDY1>4d3F7>wi}IU!HZ<%55ARoy*v3}w;)fGt>oL{74?=5 zSH!CP4=vY7NIIYK^InNI+w~f^$Dhr3PIbkX9-QFQSD>l)-e`T_ysh0+TI4+I-FT8r z`ORr27f<%h_;9sq_wbQ?>O1-h@*hjrT-z3vo3(lZ|FgoABSg=BJ+W-nY|UJYdJCEC zS#Q0qURFEuM^t-ERYvOCL?WnMlox0C6yziZvp`X*ZE<;jpcA2xc% zi0rWAr>_sWv*%jM{%>93a()LK_gR+JPg(Z1bAj#6k%HO{>3o}x%+aZ8ar30!Mc0M1q(2MDo)##?LPEX^j!ijv+un!>*CcAL+*;B9rbRe)^0Lhe=XJGt0%lcq zSnn#%pO(Rys5|b!BcAu zFXqg#j2-vY&a*Kh@6!{#b=9$|7L~?9Y5|hZW*a_zX8q-;Wkc8(|G>+Wc9$p@>P4Q` z*&VII*;>~3VtQ4(&fyd7-(N^v`?%JJt%s{!=FWUx{?jvz-S#=Ni`tLe8o7SciZ0K| zY}?ZvZaaM}t2of}z4D;{{LIT*&AgjVJlj;6%Ev!kHC(UTt<*O~O>@gBqmmiEfnQ67 z)>k+NWEXpgx!h*EFzKd#!=1|;cAq|9N?exBcX0mufjCNH~5- zU2895rE;sTGymRbWBST(<5T)K7&_Y>jz9Hk>x>A^<*{*v@@ux(y~&JFRNZ~##UwPV z^b22@$LhN0Ka|*>9{_CU)N+b+(LO;21O+}yO`#wnxBj0h**@~p>qCq{>T z6%{Y3<1tQPvq`EyA-U9o*Q5G@j#gD$neKhf^;3+6ybZYbDd+d=EqMGx`9pW8#r8dB zqDP#)bmvLUNU*H)Un_01?^fYC!M7I~T9W(knBUM!KWbNS{LzfH(bDDLC$E{i zIzhqu+G@)e0ndva^c;qjJm}_rK2AaWWQ@~*2CWS zp0E-$%zc-%?&9^ikJ3fEMhMrmsC=o8JnyxCpJ0)Yka6SB)Mp;@)sr^=Y*A^|cbYnT zq~ENj_5>+f@9n+SvmY59+t|8sLpO(&@ugD%yT^`p=bx>YAz-7i<&Ch7?1-}2a(%wL z`}j;wns%D}+yxx`~ zk9?uVxF45A)eo){`A&D-|3KpUr{zv}Z*|V&j#sYRAAH~E%<`2Bq$bE-tKS$~6iUA_ zqi$*x-Ekg=X~Z`-&&M4}EsYOi?*FU}532T5^_<9Qx0&;ym}-sfk&C-8o|x=XR=2^A zc9TmvkuR=VJ~rucOG{CIWOgjgXvMx<@6{YOa{Z|r?rYh3x%cc>`=e_Q`%k%%!j!)LlmFFzf)uB|J#(^B$+@2TP@)1+17n`Y!0-X*TXq#j< z-$6X&$>q8GTef&EFIW}xTIvS>J>xPLm#T>$g10~QyeP2r$*t|*LpwC@y$RpV8-C%Y zdqs7`#*j;+JHKphDDYUAI@$alxqf_erhPH`DZ1k#kBOyW=*8k|^xUF{`txe{$j7ND zC%QRbSQxkJQFp0hW@*Sfz_TM#;pH~U4y+TqO_ zlSgnnOQoLe=&-50`N-{%ch0)v*+;$$9uj=zB`AL0nr<3Bdatv?i^GepZ(BNzJa_$} z+^pe=k?Ys>uCUtloqlW5dzN$0#B&I=+YHAzI5miyOh^tKC}Rdn;w-LWC4_UZLQqQ>4LB^#!;orp|b z@-nC|V~0WcgQBHlANOr_y_nn5vpaLb<@wtWe{{MgQ(>AR)|;oSNBeMR=JqXn&n)^L zQ@x1Xk4zMZt6Z*}yiQ5aIY~>9a|O9SS*!m*!s6Zy(~k{u!h4T4>+4LJyHGs-`SE4z zWummtf2yeAd*!@pyXsZ?JfB65v)3ewaV?o3p%}K~#%s;vp{E)z%p1{{-#t-j_ z{__p)pEL;nAn-MTPwZLk1K~fW@Nxej?L_b2kk|=8;?O_tKX|ZO2!A2)Et!1evZ_J& z`+z?K_@oZYiKRm98-TA0e9RlX$7Hz>{%Cl)XfTBS?*Tr%WBZjqbWU{t6bP~7f(_q{ z!XIel)`6aNTE+B{191I@oZ@J*O}R=L5fd^O-B7jwYsI1+vm@Mi)aZUcVh4s}>5gkKAMync}NPw-C~ggfeXLXSMI>hxk7Ud;{>0dB^!P(EN7;ANMb~jr*1Rf$|T+m*;r>!+4NO;)0LAEyS)F z_?EzDHE%?e@Tb6+zUUwMtj3NSgdYHW+<&qEgIWJafj^Y>|AOhC^qm3x33A2>!|Slbrt(5`F~m zt%tymk)+WKfRF1I`_AedCjKu0ANOCJ{{vY=gwGE@lEU>f*#6H2z76nk|Ae8S4Rrlz zNi)U|`H)JM3yEJvhH?K*{0}sLg}`@U#t%zr!274LEMxu-Hhz2Hi;nC$@(KP4J7{Oz@H8N33mc3WUwLjvOvK3hjHWF zGh?v>gdYNYQzjo>hu*r$k^r;X&l!UMQHqTD!@;U!rXBG=7x+dL{{x+Wmw-?1|8f4n<$$F^ z{Oc+G_xc^^_@@Bh0^%ogm+1Z}2x8X&eEj|m?NI|>2eVWNUuP0y|HSdf`9s?OBtq=M zfUiTz|DSl|JmKF4{!%7?FxM{?Wybv{$sf`G6B7Rpz&Bv}$LkmFBdip{&tdw<`HKg} z!Ac?gd%#x#|5(SlJ5c^N;F~b{$Q|hXGXm$h{z*Kn#Ev$^ehcu)^XFjZ&mG|Fg8#p- zTW9zei`3x3A}L#@r@hzQ;O}t}|D#p^dj7+5&LKY*a?*QKr_+;JRN^G3kO@f%G1 zD}fJ}@c-<8zsni*9EtxH@X7lpJU`I>OTy$e1OK@1;4x6XJMeM+lXwVXEWazn?f~#% z3;A{bioOTB|2+jhxqhJLK(8O75IjubIGp#$AE3TDe!UqBNq>%%c}paz{mX;=N@uVgOx)31RD+Q_qV0MC;4Z^FR>y1d1wFCe{3V| z#O~ja*x3NzfI0ryHqicW13u0_lKX-3uK^#|KZ%Fnzr{`3iQN}w{DbwcGv}}UAJ^VM zq#t$NLXA+bFKeB3{| z!I{NaSXWdBnyS`mEqyR2*wX%#~4T)f07`4Kj6b9;6HrQ&MMzxK4bid z|AF#f!H>7d^XFj3Pa3}bg(*1b_`QLT`!DXFtmY1LL;86H__+QD+y6%38w`OjY4zXt zALKfKG5)TQ_^p6%1pd)h5)Sw|HA{u?w*epbufg{JE%4QWkMkZiiSD0*Aa;`Q<2Ss2 z#p^z{4YdDs;N$!!daTX?Vt)bn@CYA={l`Mu8MS}^OYHiAZwP$!%c^fwC46n0q2)hx zi2TdIH-h*{-UWe$}>{&uRho1?*49>Vi|H=F# zcC7N1mJW@-75ENA@ITD%uYdm@`#}0d`tff_?81Og@;8|K=X1cv`G>v-!^bc0&6s== z)4#<`Z6~&7z&8Rup5uk%@8gK=|5OM+8Tiga$bTpBJ%_+wXb=Al_95^u1Ai#~6_(Rz zjzjRj4){Y^Kb^qG-;ex#+*nnC3SaQ{bSr4ar|N5=Zc`GakLpGW_uMfi@u zp9cQPwUh7P2!CrQ{GGtZ{-a;%zuEk*O87j^fBpA&2E%s;K5W5*_J0NVFa(3 z1U}{;>)ddR{u7aTp77g%53gW<-G6AqInw?o8DjU$hw=Ue*A5RHWFE81SMX)re_;Md z4*tZ&^ThutKZbv7L;tMKW5O4Mn{P8_{!oJp|FBR9-wF8m{Eig#&1xSAKNa}1D16jm zr4W7n0vOj1!pCuBr4awKfsfY@eyp!#D5O( zspJ2rxXF3Ke+qouzi{s#NFjVRI%EEl{Il8z!pHv?3F$v-koG^tLhN#ZuMY7e7sr6a z!7Bd=@G*a6-3~N9!vDC7Q<%>RMrZwc`6`GdqybpI3svD*cF4dCPa9r-5`V;9D{I{}|}MG!lLf@FxQw_iw>}vHhoJ z!q*A^>-%f$!$8O1ANb_{b1?ls1bpm2#yA-NpMa0=|FA!-#*V&7|7{}vdVfd85YPXq z5dL=HF9QF>H$hhU?ZC(NgZ4Q8S=~d3|H+Ym@kaxHp#HspKZofb=g>g;`M}5VM;U8~V!IdkMi4*d9S?ku#Y!RkX5dc) zKH-sZ_>%_Vk4<3oAMNq}Z=n2@z{l}NKIU(r{Qba(ed8A&$AHyB;(rQ!1K_g*5$Df9 z=l{614F9Ny*YAN8Auz@R=bRWEc>R@!1Iz!HC=UAiaNwGs0|%CWLhKV84;ux92 zfqc04`GwYi9B{zB=&yo^b800VsN)0&mfs=jI{&g_Ao5({z_IaQwj-jQCmeX*n~A=F zSXe>i!)x1L1rc@NHQulCPl)z`%=3s?hu1#83L@5nnFy}~ewBYh9ACKH{3`#1s1wIH z{aeI%*D&o6kq6WGS3$<%C#G*TaE@irBv4SL?tK$9EHx#|mP9Gnn>NM4c=+aJ}t= z1Mj&{!-4au01hlv#P(u1uwR$qz%mfT{$BaT`ftQI%9!nlnAZw8hQo0O4lIc1w;C%@ z5HX$yaG-rX99XD`?T_Fzd5E787-tsLMQW$gIx+gnwwp z_~&nkdidW3#CYceVxI8-8-)cC`@M*XOPK9c#HOXpc0{~JI00glGqatF*yO@&ry@4F zGTZ+N$HDnH=J|g@%*R?dkNMxqw4)+6r7+tO@nb5pj)?tFW7da)n7=Hh4kG5`ARy-X z2(uj#<2}x-QxWrhg4s?*Y(K?pN5pu~GwZ)YG$~-(A)-zZvyO=SP&wduz!!iRXA2HM1QNKX$+$?DsonJ0kjd&#eCw;`xto9{ceb5KX@@?Gf?(H)frRcuydR zCz*&JM>6Y(NErn<3Q(5WPDMN~2kqET1*ZK(Kr~fi)+aGh85^Pe-$aZ{jp>()I8Hju z_WwU2#;Xf)VQBjB2kS0V!s{%;;9B^`x9pU88$M@|DIF+`?&@4=giCv zBBsg}4$P+q99R(1?tjlIm<`7B3g#o>SB-&~uXS)>`+8Py#M4WFG za14h7pHHwLVm|-(oWjV@|DIF+_nd;)&;LEAFxJCAJ)b~&em%e7e)7NP6wJndKL_D{ zN_{@T^H~4ia|+{m<$up9jOP`+zy9BI3a*a-eqO=*xBq+3DL+R1K!^Wt7SLm(F?6ZY z9A)DV$2`0x>>4yW{UzI``z$=;@FHNQkTbmWouTN)R1MZN7~d#G}^q|bg6h=DZsND#!m zixiHD9cP<(LR!95Nw>LlONcC*b^3mF(`J(s5}CJ_g!o*aqt~+ia{czYwD77-_q(DE z782|Gq}{XM9lUAXvq@WQ1@qk%cN_@fTqlJiP+3&@-pJnc-X5Ms*%vE{oq1y~tE~2s z^0jCY^?c4ZYt77i9_v={no1VlwT+#w8f05Ej^mWdnc-?1cSO|A4vYAe2O2kO;yW@@ zICwVP81J4UajyT6XtU7iH|=B7O{#c*-s_mseq?Rt2`6de{+L5;$12X)=4749Epi_@ z`B7uCbS7tqFmIMz#jZQ?OkKe4GkObI2--^xMFP2qNS#LMqEE{ zl&@NwW3=P$>?QX^gxaf}OvLM~!;R{EzU?zUclKlCV!3GtGg1!bFvjiw@jEGwP2)KPHeC-7ZZ@3xn7&&Nh5GcMoCxyeR&GqWU;UqBEYRoOUU$~*ds%I$eZJ9nC617qeea@fyB@yPNXak08zF_Ght79%bkH~l zx#)uZsXnoy73`H2yQ`I_R&dU9Jt%r@{kluX4QtM?sJ+oU8_u=RwycDoHX0K={* zWs(^4j4^K1-&Ar`2PN{(UpQR-u8s`bxJ#dQ+I}7NsGxnU<)%YleAjDckK5&U&iO`j z#Mc+8wc6R=V(A(?&V4o&6``%m(%;>4takFRcp3W{e&IYURad%3n)y&+|C3td9MN3!s z!ci}EJLhDu_tcC>JrbOU7dYtFRhE_&-<_co7Pmip$6{v<*>CFSM&BrU(z2!V>dt9L zqqBDS`ZZP?ZR1k7w@puR3q==y3rPycH^;UZE|s(=awd01u6@38-S=`0f#bY8$LBu{ zUaGM}x$w-0?qiJ{PItLe;;Va_N+&m{I$sc!FMsQPdB4_WJ&96^?ieBp$Z^u4vU0{k zUd`0omHQNSefw^bJ!aFLJE=M4o2z!~(F7;Yb&?5s$#L~?mEN?2Kb6#u;P zb@!<1C)^KjU8Cpmecg6j@p~QsqgHv=1Zk&6R(=rN&VE9^?N@uWW@itQpNSKS0;hUg%!|u_^wOM1gX*{j8Si>1{Yl*T*QRw#H4O91g@T6z@zEUDjsn)KY%$ z+Qth7q3rR+!5XH9Ydq%&)~(b3Wp$6-pm`1=l0IF^3A zBH43qe$w^iYwaJePvW2_ZFh1V*WH!2yES}LTa>?eByU9Gk-`LJr?cEThv$1*tZi+( z_4JU8!KM|vPdZL`MA60Ha*)EYQ1RNde*V)Xh3B5PTXFxplQBOz_TUP)x-cQe*Ubo-6zoW5&Q^t#T z*N?rn=R_~hNHQDs!RzLfo&v^wA7lT;XD3oP+DsGu`At5sI$fQ|+PLao;(E6lZrg>bYFTF~@k$d>K#ob8*vhtlrZEcqgOu=V5VxAl~GWvRMjJcl<(X(&EA^l52x zSN-7>+o#g9ydNh;`Uhk+a0odyzc{$}!sj>2DorofCM)l0Zfv(ss`6Z7v~j_eOU?6- zu2@ddm80s`l~#oh3zN^2$-LB=kZ?$8KijpYqW6^(H>N%)bL>7fnQxQpZRJO?k(ZXd z9(Q6!cu}J67!~=8?d$@E&x%TKPyyul^>nJ-voqOW$ z(|7x~Dd_lcuiG~)f3M8$HPbo11^h^V!uhl!an?g!nOlN<2?^1ApO@Pmt)b}3Q*{*+ zR%cB$G!zvro6WaaWX`7v!!!I#j&PcBjWEh{8@t7odmZ=wUdwqRUpi8aJ_{EI-=6NT z-JD;mq3BMe>edJ??3Lbn*+oNI)J|mo zf)(x;!;Y-7yZPzikEADh9SS-P$o#WUk=iL!b zQBhNJ3^I32wEY@9Mcew|fgI}fSc$4TM)vyokw4c&8Eud$->~iO)yIkYof^E3<*C)d zolnMAyoe}1<##5=u9#bWg0ERd()y%p2FK6#o6bvrQI*VlR(T#J50j|6_j$s!#pAVw z?u2lb^lI|DSN7`iaE?t%jIlc$6q?g@GLC(9ahPPys)wg5cBxAxN#xky8fnL0VKRQa zg^QDuxhF+enW~$c>faZCFW}|Qu$CPLm*3@N=4F50c&bT1i#sY{@9c zUFf2l%N){dUbEhM^EhKZr-j$OiHt2iz#=Ie1>?|8DU@&QFxm8!dO z_sPQ2!eq0whcAFIM`%|1SUMFpXvn#jBO_=~oM9 z*lA51B@)^xX}Pv_mf5!1Dsc~rGwb?jj;pS_@;`o5w1c8MnW`Hmw5M_m zY#T$-Rj2B9_OCy8!L6mxLoq@9;e76|cg$BvI@-Ic+a-V7)L zGh9CMWKy5@>#q{UB37>zv!3(j(L>X`kNdXWn5RNce_b8hV_uADtnsIW;+TC=*zrHL(hM|t*6w&`2mdv-{q zjj$e{a!9y8p!Q|dIY+~=%N(`uEjEqGn^qM0z0IQEoT95q)s1eP_hw|JmOUHphPaG)iTG=)M&(no^hQM+s$( zpyRHE}{qM|;erXBY4{X5@M?{XeR3J>Gc!)9l30k;xOg-3kmxm}=_1 z-nrB&_(0p0)~8PoXPjARXO<)4xJx|!T1DvjLtQm#D)%+>lZM$8KiPXsJ(iq zs;=3nu-Epe_dd79 zJtykd$WnCS-(C1m;aHT_rp6bzZuKUw+cD?+PhCGD#s2WvxYc)fg2qHkee_%XMO5$b z?NqM$Ve;p0jms09GKNE}fL_QSn&vQhRC4pGAd2qH|ER*aaa?Q~Rc?DV=12$I>9I|l zZ``i5$o}lS{;kU_-Ssy7^Tm?$o*208T==HoSwQWZhTh@q)%zP&^X2>RwyRx!>AQ}4 zy_iMSZG9+G=~wvKO0oLYsY#B@Hb{KfFQsR)Us_~ru}$xH{xF|M;X&i+`pXKx1-(Cd z+VAU}hVFBpza**%UGg}lPfo4KOgS_|am*w?P3pMmQFT9x7P==y zsJV~oo4atYeZl#z_|qlVV}*jF&Egh)`>M6)!ToSETj;x2p5L8+ zqhGww6F=K*IonRp%FvDb+J9(&^3|QNy6NY%f{PSg1FEj~Yh(G+FODaLANaqVBk7k+ z8}VZ1aLzOz?UhQKS6;5FQ_pOTE}s6VTj|UCT9wkq0&N~qc;c0AzmzXM$Dhkt&v_|g)w0bw zZB5L)_K7#xSJV`FZ}F))a+0EJMAcn0&F0nUt^HN+ruuti)Sd`Bqny`9Kl|ioTS5Jt z5E0cRd51jFN#|EO`K9%>%SZc6I@hnXQEzdX49D=-D$6&8Gv0eL=EH2N?)>mKw}YoII?3zVDpm;$393_CXUzTWQS`ht224 z470wST;IHHt+U|k#rK-q4L)&;3k`d}!GU^TZ9&z|n6D`w-Oysl_H?G^_zzRl7Snyj zU#YbAUFtou^`7o5^V-;g{Mql8du~55Rjg&*wC;d_#MI4oMN#=QZ(BW2d0|StbE&!) zR-|aVe9HFiyzRc}aAj&zk#p~3x@c%r7V-Cti7T6azfVWn$8P#oX>8$chTtn zK8ZK0jIw;_XO&FDODMYYsJbc9K2N55IX`qi{>)oY=grDbq50!oB}#j~@F*!;7ez*% zbhx7sINxc$<6@_It@(%4AAZX`d`L}T%U9m9LDzyjQwE&RhQ$8mgNdtdwO^JCHd!*3SCafem<^r zHR||S5s_^cn+n1nt~ty8Rq^Wy;d9$wuwS3L%qeL9D1XoRJPXaMyqY&&`cm>>Mb(`X zJx^_qTh`vn@nTKAY)*6JBpineABw$n#&unA{`2R@UE&ulwjA4^x9`ck3*KGIa_^+} z$Oz0SlHro`vN$_IQjVf)P1P+}meD7E;o>}7>lF9gj@aG#%a3ueze>G%Zn4(X)x+3t z`Myz&mXmi=z@Y*I8o&&Mx% zft?gxTdJSObTPG58I@>sxJBWwosm<^Xb*e z7FDl5jInR~%DzwWrKILj9+R)54xPKR(m&{@^10H2=RAgPR?2U4?;GEm(rUh6K2peB z;FQI^_d91lJ9C{n9~M*N)mu~@VZ3R1<*HMk1SO9q+>oUQeekil^n7eh{;Fxc%}Sy( zj3(bWIW4^7Ot_S;(iY2ZYYX*HDGl#da@c5VhD1H37Q|8bwgv==g&y*r9r zg>#3^YaL}6QJ?6*#{SVm=jW8my!m-4q8}5AGRI7rJKj2Ni|R40ynwlaX~nx{sy?IM zKP;u{uJ}ID?rrMn&X;3uUmmu{K3QI3SEl#t$*T{)eb<#({Vm0G-FbEavp}CWrCQ3n zFCKnuH?z_9XPJ}raen{%idVWvQ{uIw>aLDT`LuTGC*8QUqF1EqjQW=IW#k(1ENJ3? zeM^&z>y?E1%u{nG>FSQZYwGy)d!V!4S1~VsS!sbI`HQQx*fgVGP;{43bvN|o@b9y+ z`@uc()$v8kTC$^-ajxJVmi2Q@PR-+juO4e!H(v--ceXx~^f<}l-qjUR!CCLV>DIi` zGrzd>XS`k*T;%_AowujzCI~*C@M8Wrv+v1)96IY~_lmz!pDp9=mehRk)DMH3Y>_sn zM!JX7&PF9|R*O(jo!NBQL8D8n<3q}I7rs}T*DcE_y347$=RK^QK3`IC;M!j(ptDi4 z+(=M}zRI;_|MOeV9eOh7)^Ja^f5dglDBX8?^^f8!MdTxerHUU~OO(Y`O~$09sdeF?NUzb5ou9^1Fxq!}TFc4F#t zErpM_YHrx|$#17#`aBbRDR=UtizWQV2Be=@5}@NQpHmd%tv}4(EtN7A`v(_BjPh7oVw8}5L3JB_`oy>;UWr`!Tnox5x+|%=DXAQ#t)5G# zRgO%2tI_Qcqqos4r|q`bJ){2e+#|EZ6qfi-lZ)0>Jt82^p{rxN@T|d^H5!G_x3E8t zy%gDW^|3NV*O99G`P|f2mko~{Hp=f>utY@fR?fv0^#H&tI>!N^|nOv>blVGzq8Y<`xuPSE}v;TO)-7Kes;5 z+H<{oTVrz}*KN-C*M2TPPe1F&(^_G|UujvnXX8nJlb=)Xnh1HPc#IL;_-y`8^Q!5= z(jRh%IVn(d-Ke^*YKGxPP4W@y;s+;Wr|Dld9$B0GZCd0_kLZkz8_O~kGczW?4wVa; zcGg;7#LK_7sAzbZ72C?_Zf^GkEpdHr>T{MmRac=(q4!Sgk#_dV&5~1POoZCf?mqny zzI9XM>4O>jYd`C8&+V^nRtkLjRH^u|(tM3?rnWRbJI)u2pQ$}7HU56Cj}os3Rkz~$ z`i7e?&R?GQX=$F|&9!AY2Uo>@$(-&wYIXCdEBDLq2S>5{rx$KMW0kIdfA-e*Pm7Je zDbCvz)ch%@VVUHq@`V&#PpYn+*{8ha%ZzG^683krR$UU)5*a%y`@Qnq51OsX8614N zH={~rH@rM@{yC3rp6YOVDKFc_N~480m)hr+U-s9!vWcSWMb({uuS8gJcy*b2;Rwf; z%hIMF_x48E=znJynlmNZaEVUFiS%u04%3ZOXSCUGQ+T*79c+CHXQs#t9O)Urzh6|ESvZUDu-JHkt@bGUVke zGalWR+Sd@PLc8Uc{`f`XSlykqlz4rpy2eih_~cYHMZ46dT`ZeV?>V#fl3vcw-CnxZ z(giUEma>&;w@tN|WSx#l9%ski+TCpu%+X|c_Rvc~flIFn?x=33==xH1k4-9QpS99u z-6?%fZsQ0iRgZVo!}PSa2W>x+vhgbC#;M{=weUu+nchYtPW15y6-;d4eniuPY7hQbOpXqbbI^h1>sIk7KV_K`a9_x$r z?-B^$lXSlDNPD?d;?{=PT)Ep-b%&30ELEh=Gk>bCPa>D$!L3qz+_gi;X0a8l;(2PQ zCfnqoW%>PPF1y}Yx7ov7FHOHzBA#+b?bLd`xtt4%!j%u)-8S4V$VxT9s;Pk zoRW1V>O04E=RFxYXW7jub8~NwUY44Dx$OEh<$YXVAIKju@6Q@8qOyT4yZ>m&F7GQ- zcUAUDE1S$aHhaw7*AaK_P;>*Sx^FIuj^R>%TzXr;dgF>IG%hVe`JC+I<_pcX)KAL! zIQ?RHyt}(gRqIrHnD*6*Uc=`Lzoac4_TW^V@ZB--8#cNa@8Xk^Y?H~W+p=%x z#R{=HO1!~T-SnL;t|#7fb6rwv<0`9p=b^I3=DI}l_^AiIkMK^@1``JHB zMoCmS-^HP{JTz|B__mXV`W041K~+*QhsB-J>X$D%mV4-K_`JZJB*$46e5WgS9Bmx7 zHTjj4xwlfvxf$yzx}j9vkB^KZH+1xMy{#PXci4~5LUxJ5hY;bWd`)lj%4SXvo0HEf zc1*jRpd%D$(GE(H49^xa6GHfm?SsJh;LZ2 zWujj9!jLtV_X{(8`%Sfybi6Zo3MM|yRkrOdoT8J)R*>>3MNY@Oq^U{a#OZ))C5mo1 zRX49P@#}+eyIm*uOfXZwJ#JV}(acEmdn-(eZfTE~x_Ws2#(mozvr~+|nM*|VR7Cj9 zNULtw*AM8IP^h{fv8rplEk!qis#~pL{duC5*;G3HM!Q$@H$&cGMppzbYzhlmGqqra zRm3QdK6#sZEe;V!#u6|j++WR>fSv$U&y|ZouV5_)eTy!`+SA;iR1o} z?yaAFHdw9OXPx_W>-jCm((BfmyG&a;D!Nt1<>C7$n%%L`_y+HL z50%^y9g1!gRo72QC{9*R-%tPi`E9x`92;{+F8pXWT1;)kWAn+a+!aRoS8WVFaGtj; z=-qUC+MGr052S6U?fJYe<&x3oWV;3>>V4EIs&0e1>dy-=C+?RX`OWx#!T8yS^WAdw zV))7e&tz>IcmBp&!KysoO(R__^a{W4tJ@r}l{dz(^~s$tty7~Fb*ysZsn0{vRNW@e zQPq=w#Fj4W4!G}T<34u(EB9{0TdNhDZt)#*ai|Xvi%I&>`g*-8f1m9Jz6*_+*H(;j zT`#@i)K{Am@6GHJR493fq3TW^ZYQ}TOJl7@PxSN+2hPrIlUlQwKEbCwX4tUX=Zmkb z7eDcq!;|NXP4h(#?d z?lL#2cLKN`K=-+u#qP+DgL#7zRUi4I_Z6W-JrDe2(B~% zmwP=A%vw4nL6KEm53W?I2AkGeAaLK*6X@z7h5pbhftyIa(`k~6RDAy{{&PA40#ZT$ zhI#~%$3rr>&qp3dI3l`0oG)Ddnr<-{5&eF+Jo8*q)iNI5G|y0ge7%6KfEm4-8%mp? zw&{6ny}2J|3wMui>M+PES?qY1RbBz{I8jR1V5CLa8EJh;N3W$=N^Y;|7WUFOD_os7 zbat@-;CchyT~*u0njwAjfrauB1wv9Mk?*5r#=YR@#-v;WekWGVpEH#L$jT3QKJ*`4 z&G8i59{qcJ?tySBaeJ++;F7*PaDUSW=+Z=3yzHszSU^@QbW?s(TJSmer2IkW@YBK- z!~u%avU=J()-TFI+HR|f?`EL_e?~KHJ=a3S_bpGYdIwnG2A~4+^#!_A_f9(#2g} z$2)t9)rst{nltRv*(lkzg=adz``91oE~C3L7nA5+K+;09ZNtqjPID(7fWY_Vi&VH3 zgf>n%_fa|&zv3=p`QsO;@kl8#?rnbhFldiA1fhSDj}2-W1++r|(AD}L=UQKd=XJH# zwWM&*;IkG^;4-g$5}eO|9+QHmzCydWz%Ac18sDizu}fdiqJFN}>rpb_!OZAn`_bcL z$soWD1iIl#ismn{%JXsRP|>`hqVy1aDmZ1-_*WZkRs_%tbu~i#I&py<^D$uKX<^v2Ga6lATv-1a?BehQyY0Aq0Sh~R=fHh~^~^{x z$UPF&VIwl8*us@axG-2%Zzsu$&gsH};ipw~y2}y&xf@t72?n|oejy8}*6f*Nm%^Ky z4+qYLm7JZN9Ar!#qgDZl!lBx+wJMsat8qAAwZuwBe^TKn?ReBStO6p=JzW4iunMMY(TzYK-U(ML*%`x)oF;gHuh03 zQ4s>PFp$ViBN4#_!tK~40_GH9#F>~m@K!Xp^{Hc!zgCS#n3WOCBgii^ruYts` zW)puN{EAnf6&qwMr(ft;nfm%plQ%a4=(^ige0i5u`y~yxf^VvmYKQdVkNE?7u&B10 zr@VHZ)m-Y>ZF>9ljOK&;gmRSWv(fwGTGVcxu2yXFkLO&uUfcjT66hwCv$~rVBK|+E>;Irr7YppsX(JJ_~&vKc@9g+Ba zW5E6H7@$j~{mnMxa)cT8gZSpxO`^56c+RXagmoEBjVeZeUfzq(E>1y?m@OJzR&)`S zVt)1X!7VctH_OWJT^F$OG!ftd`NjfWI2pxudzB|UiJp&CTZ`25OwEr&``qUBR;$^T zkVZ=NqlG=WsXNC%)MKi}iS4re!!M?BN^i2Ij@21V*S*>80B#)6J(0yE<`0ua9RRga zizx08G4E?N&7YScPnI(6GOn;}>feGwA5H?H9P^%=3!=bOg?kZE4c&w@ewL6uD)yiQ z$IW=4+po48L~$KVC>UqqZfYdo0)z7LDr!H$IU^G-ZtS?=RLf@hKvL4*f-{jrXa9F+ z88H;^1sC#aS#019-qYYu;JP~j=>B48(FuZ4-nsls_g96&6P3M$NE44PAT@9ce5`ivPp=UXIy_KA5#^GT; zJG<=%1KM?hdO|ZJ>2=cH&u|M@O;4$YPV@UTYX~n#}zxpIm%W&2J!p zn*?;7D$K$anoBP0RYVNgjqOM6*gpNH?lC)4;gs4@ZfI(L4pN&;@%mdFx=-Sh)jy{~ zTkfUAiBvmRYK>qY`+YA6;3fmzX-qFBHZF@vdY#Y_!^BUKUURA&wqG6LoG$h|IAa+; ztRX8W7d9Eq>iOIJF)o0<7=qTeGe=tgyIrA&%XZKKJU^KNbRE9Iaj74td;J*9Ff@wF zL<)O%O*jV$cX>Vqk3>0Qk0Qy{3qm4I80=cKEo4bz8jh`gqNd4h#n#*aH}9%>0`8-y z0^O2YHxf7b!4HNX0>6=M(1i{56>MJH*fttr-W=yhMVNQ-cnE224+wmK!jO2RkZV2? zW)j=Ol6@X9#QA`Vlz#zehcuw;{wGHe8pm>tZ&V&WsZv@N#B6%%4e&7NJg{m zd49$eX?zO?l~iZtI=s2Uf6dRzh-K)=|2L@fM<%d8%L2M9&pDDG({B285f6SdlQSIk z38HPGrcy~P#&v#0W^lJTz35z(*!!8fMK^TkuFh|~x!7A#YvX`oD-o*7)8Y+0$Da*! z!ycLFtDL1MB`eEqW76^y(V6rWRsG-hzgyM0dvA zta?E*U*cv>L=tMc189dFp!==brp1f1SU0*LYhJ8_6Z}h?lfdr1`4u9G-7OSHpQFRa zkJ0MvkuK6q4qO9R>>L};D7J-}Pzi%d(jRVZf$pVNP%SMTDz8w7NlxeMsq3;WgsU+*t9`g3!p|apL6qp1JgvXjT46XYs=7LWA#UcOQBl?RMZtc{Mt#pw2n&7 z40vdqVbw1=`2`%`ihypgC4VUI_)apHTN~&*qi4HBXwi3Y(IgifK}!shVb0vjAVOg| z9!d54B07WVPL~75iqD0V@^BCPf~~lW!wJQJe2amuZW0UiNEC97-XJY#S^znV;o?|K zE6*;Hc#?suvd>3xPV?aJ)}+Ys9~%}O$lpdNX4cKE!@uw)aV_7a@HNB$=aVHsm%Fc; zKyBY%Td|#3w+}sT#d7H+>2i9OLlP{Hy@*UG%-MiA*dnG!sc*v@*LXM4_#3Z(%1@H% z%CZT32G~ZWZ-9JDfo>hK4P@({cMQ`9q(8mI#m|uyR2p5%lSFMoL(jE!_a%bHS z`KCb(QY6h%B#}zL^uE6q?pV6CpzJ1eLIYlBWk8pEdyoF2E+qk>@s0w<_PQHEQiK1! zmX!$A=MH%KY2E7r?hOtvlI>YY!w;Iz?ZfV0B53NVNs({mO8G)Bk2vBXK9N`#UARai z)D|2PeHPrX1-KPJS4K!g+~e<%kN5fc4Udt$CMtUF_7L%RJ>um4vadcbv9x91l%J>6 zyO<^gz`b9NyCA{}i8|DTpz^Fxgl&4gi2-gU(Dn4&pehz@)Fm8N5QUua{x$}d zU=K)XDIbYepB+nH4)2N}QX-wMXx}-0H=TdE3>P1iJ*iL1ZBX)(wqXl!xpHBdm`Y2z zl_|en7|{Z_)j+p~SGa(!KV$5w)D|?yO7df#o!>+wgYpY*bIRfBz$UIbPrQ?@p|h1S ztp*(3&Qi@djjIE7fW$U|2+Cu6VcsdgtpU0w5fgWrG=h7Eu3Cdw(CsrZfjLc*y1{EU z^@WsQb3Oiu{iTbp%O}!qT_L|df1|79K!R%OVIju~ZrQlZcxG(jx+@rR;(XX|*6i~9 zHKrV00Jje4LIlAjhgg7|a$|Y?a)zw=OUX~S&ow@l7B_VNJ?FhzR7YSZp@eIp=r`Xc z>m9CDxb)^H%xaK0&!D%)WN_bN0J!x)*Xh%Ab%mq_7<=l=uz@Q!=t$1(2NT`oOiAR8 z>Q>P`;}D_=TH|H+U1n%EC4sE`lA*(v3197x&UT*C#rO$*DuCMnbi-34mZuTKKa_d% z#!%EqD~bsjw@D~yK%Jo+i{c)6z#`7U6_IYBnH3tLO;gGDQ2Vh}qZw*8>#?^RndH~@ z_W|5SpbIhNJ#y2Fo02S=^CPKE3%{K?8A4%Mm?J5ZdRj^bC4y**S6;mTFA*!C<)zatYOkaw#@GE>)A#!8q1 zEce;C+gCU|0@#we|3U3pzFNqjCFzNe6anf zuTpG|=Klj5YasAK6;@lXRTIHEiLixo;tzb5;bab{{+vHM-Kh=%Qstsp_4e0H_2`dV zR>1v>cA!f&)}QIlBsHFErcb@b9Pgq-9yN~A>gh_r0`dDx5o#=Z24(WRk8s2OyPjs* zTWkG9KQF46CAMTCAwps#%%?wqd^>j~RKVgI&D0wfH&<)BQL3@ru|*Y)sdI#m&=NP=*}b@4@_N0$UQ`+Tq3r%R*{QrQ3QF|b z2jtrYbbWmrf@b1O=~|f&`|WWYek&TgXF5e32Qux8#>$5voG@ybDkuor5(P0Y2!5JR z*+6FwS@iMj`F(7G@WdU)pbc=lfo_IhzsPWYAywYrF#UaDlr0I`8U}d!GVtL2E;n6N zbfh-z`OTlgi%A!PX7~tOx=E<1Mnc&YuQ!;OIMDyFT7c%oRbl`ZGr;W! zx=3dXJF$)k_{ULp#t(tvSlrq?>ZHQMC|MsgqntAIFDMy?)F@&;2EZnOz=F{2N&m#` zfU{=$ek2$AD1)n`Ck${0fG&OmP2WCsQD{b*v7lLA0DAt{o2;LB^e9@P0vH8|O)*kQ zx|hvVv3t&R?=P6f;%Plg5_YqS=CsB|?o1>XFj0f&K9v{?90YI|Ou9 zhRH4)WaT2_WOF3%FD+2n{Vyg74HFba7Vtj8B=7%jNB1d4@Nun=fB-*4!*gVZAJB5+ zc}IDF*BV}HQ9}x>(+&e&+6G1kNuqU*0{*!!O8W*&+V^Ay&nX+l#8Ysyzq8kEIE};7 zveoih!9ThB@z27UiCfsEYl&C>z^tLp2%?tT1mrscbWNlL$kq`Tbe0py!Sa9fK;3&J zdx81s8OcmXb&FJ1w{L3JyKo~lpqar%&I$SwV5=>e5$Hr~Rh?zudSYqr0_!ECKzCJX z!Vc=$oZK+m^Zm!o8kPBov2LA@E5&E5I?N`yWlY5N3LFLmKS$U!TrJsd@ZX8LoPO#} zlgikoge&D46i)=?I|g*KlhHUcNqkQGc^3jU?CqX+is_nc6?gRI2xu|?{&9BE&O0Qg z+}jch^_V~Dlu}Zt5pE-~INS;p7%S!EmVprfxZ^+9CGJE3|=rZ@flkz|yTtwbKOacq?V3(=yN2!Ss zD3|jEK@7nH>qe76SD0XSpx0&GHmRV>GbA5IAS>{MQ^_p#iUI2=X%QhaXBnfmhhSb8 zI{h=z*U+H3t+4m(&VJrhLebQRISRkV<^lOm0bOJ7I}H5oA5dy5lGyP&MkrFBzTPtz z2%OPX_dkcReQvq;4qt&s6x|gdGb@8iNc9wNBvjBIM2#(T!&rJJj|;4?PXpbrBxSj2 zNG@v?<>jQ90aeL&G(^#GnKRIAgo%#|`O+BFEkWVg~5)i$39DHY8_RVEWlRsT#IKy6OvBH!t;<3}-QPdQYkilk`*zA-JfR zmJM7I8Ytb&-U@j+uOwVZYMB|a3w$dBw8JdWeMghVfhzwPXTy?MGd8uNjf4h^<|0nP zIL&S_^)5ROoC7R zBiZ{x$qR7jfo>}6zI%rt8kLy6Rr#9dxC;5URh?M!h=H9ZB}A}%J5>*s(xPzoivk$l zrp3sExP190?^HPR*VD<#w_A`Kk7|hFy)`#r0dk9Vk9Vf z_%MgCNqrSezelMzeqQepttF!SGzz+YI;mhBU1Mah$D>?qN>WjU##8xvTnQ^1)HJjG7uiD zi=S}-?q8s5N=;tEc#An#vnTxSqrJ2F1tRAM`gIO?tlSFKyT0Y8uh>==n(m=+s&|4- zv)Ljyxx>@JN;q+4EBw_~f0mVj`>~5ampWxUl+B>Wgy1O6nsmnQ5$~hKpUVA5p%aM# zbRWA&99FuJZB)OY5%5a**~u%Ecg+r~jA5ju%eEO+_Vd5lfOYUCpqs~8XC5e5wR5T_ zSHU%wyVULa03Q@nRyHPtN5Y@W`*mxdokvf8eckZU6pBRQGdM=A+tp`pI0YK|nixd>sg1y;Qt8o^rrb30^%}-!qj%eh z@f0E@Ayf~ly(zJi?(JqgBkSY5;OVO*!m9FBp`}Id=C-G&22RG&29R&|iyjVdwJqdB4Ds$O_I$0*06TN^9ZE&$4JU6+3p zI6qqhx)z&46ZhY0Q70vcJw=Z*kuBV6#FIbzid*_iLS|ckksawaMzRIpy+KH_&=?+G zWgA=My=)cx%Et3`IaGQ!0od=X1Kq8m)m>__&Re`hwS*bHPa63w{7RcbnIR!ZaW8oV z4|yEkCll}>3kX!-QT%VKg=Qnv(R9!%O_`?Ias5e*VrhVO*Z{hxJBN=@C;{ErsDoR@ zDN@?JkvCNG81V)t1KBa%+N`CV9_%_2cBs6k)>d}{HK9xc*jCls#xE|4}$AsaD}J|;4JxF+YW3mL){RH*H| zY1~hfIA}f%F>5b9w)=nIkofO;oGqZseSnV}(vi;fH@y05iJrOGJLXZ=FQzd=MlB9` zthVZKCTpK$JWE%W$wc_!DABCcsJTm=GI)`qSz$t>qP7|G0QqhMUH^My;zEitG>O#R z>x+0fpTgt0ou+0Sj$!jo9k@nIDxWF{Np%EU#*+J>1WzxJlSJwznT|ZiyF_`!*?6KG zaDcl5bO%KmwQR~f+U(*}>9E)g>I>fy*0H7{YBBj@*ZKX1A#=7jCa22VDbhaxNqt*g z-YV%5<2Nf+>5t|xWopl)2d?LLfi7qwS*3nyVhh=%L0?nQaFU0v=~7^T)j8`@?P;-~ z`_vS?PPG4Xqq}tA2xN5vM73TXDwuD95Qa>-W1!K+ToEAOJ)j#i8{`>L#~+Kzi-l{& z7a$24VdALJ>!6CJfNl_m@PU`kSS)Q?I&AP3Z|pj-f4<3RijgDUa@vD@XRH^c$?*Z; z?gQO;-@->GGne3}jyB&`%{Hk`@&iYS$lHbIL3b??A_<|H%R*^!4TZ|o@w#=Oq#Cebn$>hEZBMdLX%o$~)OVTH7fT#G4VK&3(=0Nuu^L7*tCGb8z0lIyN?NX$Ve-vOKrBb|_eo-jh z#Sk(qmB_6AM9ci9E7#B6&7tSAl9D|d2-b0gk!v`M!9iYdc^&9c2DWn>FGd8&_Y~+l zf(U=~riJI1!EByr5HpIxFAUmD3nfGkJvJaK@Az%uLR}NvGep(f*(sd6+TEQ zryo?I1}OT7Vj@w=Lv8pSCUhQJZtN3T1cR6Kxizx3AX%cf-r2)w5|VxOWKAPdi#qX4)UK-Z@w zUCvBJJSePJ@~W^pp_1AGBd8W#GUQ`+fBJ=Da83%k=tIFNt&ENeLXN{X_ z_}11#6eIZ~3_gN0>sLf6EXB|2Cvn+4rWMZSc8Hs$?aP7r{UkPhzpvo{?lsW$YHmBk z!}Q{FNE0>_Nb31u5q~&6w&9oSPfm*O6cc~~$@}Y0oWngaU02OUn!J~uwaoV4^W(A9 z=OY-G%`%g~^N2S-I=b*A24<}nl$P-}H~RlW1R zm-qASncbN}&RQRl{nM-wA&SG~W?g3azLma$8)RiblErKuv zKQq#pRiJ_)azoTKJ)`V<^*mj?t4s&B&yP8-=3`O3^*d5LcgzQvo3Uu5 z18iArXMyv+d!TDO?{2Rs5e$iJ>whYB+UST~S`cT&dlOMv;JKBv7~sP z**j+VkSo7bgxDeDJFo#E*|BAS(BDFd%TqQY*LO?nj?1A%`xZ3XcE*6D4j>9%o zOk3~S0Qo)vU1FAoAb0_=%I>Jg(8b!4dShusZp`#TGkM;$L*jzP#f>wF5`4 zTS>F0jVz>cpBPwHt2DuGl}sqkE)0SF!|UAVEkHB2aOm;l*($vRc|1qkI~^BZXndel zZWIjn)GfT9ijK;y8%%`gaP`A0KR#UX*bynen|D2SJ=om@`zt4I;stz8;&tx+7NCI^ zJC!4kvl=fN!FajCv;FZl zh{&xp?;@Busm-GwEW*`q+N3>c!NbIFfV#y~Nld-nP?v~EykD05I%hDFk-`{xPZyZvVd zymrd@j!!p|jzF~W$?zCx+9;37jPasm&MIi_v_^{}(-Zq$^XdL3|~ zlW8P0k>pwc7Z&LDKff2LLeOiW%l66AsPRb8 z=eR*=NH zc%*6s`E|-ll>8WnLZ(=gi8*)ijN`;Ues{{)Q=Txj6S#6fW!e5z*V?CH@q_5wf7clp z1j;|~9neiRqihw3i1IQt3?ungvY@j+b(qGFx$bcX-Z#oI5*G}q&-<9+@l_A08UH4e z^Sr|+e%k$c%3W9Z+h%Z>&!^wes!1atB=Th_O}w1{wk5+VI-wP;)E3tnrU%3b1t#!~ePet+Nn- zZpF;Ww)eoX|I-Q<){OI6a4!*---@xN_CflBSBPh`OX*STh{+rh8rn&pB~iP(w3i2( zevC>91Tr6`OoEy?@H{Od&{YgkS`p3m!eNi4%X(S+DB%qtLN5=#)Qx#bGg7}Rb+_uw7 z@@Zn(g^9LNV-cxu6s^zX-Cn%q8CS$G^DMsHPKf**rki88Xmi z(ZoZEmaCL-nn^1(?3q4Q84HgT$Wwv&8$uAJhhBmTId}CHZ7Aqg&KwN(HQTz>vjJ9zCMHS7NEQ*n|bw1aUhOoA?gO>R%ZZ*cY2>~bdO*Bd=P}v~i8ias zs`3B1p8vqtn#@~(wl>0$11!`ezDy%@_2g@?S8D~s>V1ZUP2q)<7D70|@MT@o$ht~= zSB_^gvucex>0wVApbC|JUq0%k2U{KT-*a`Zy(AbII?!#!$zYve?eCjOmu`;bOQpJS zLXM-W5Qzv@D&Y7*C~L~s*0|VejLgVTGx1B1Y&XKQh4B5b$<@;GW;ZZ!*ui)Mzsw zF$mcyBjnTn@`d=H`#M*93y@xJHpIUJG*is+T)o0-S0&F+Dg6}7`kw-k1Aaynwq%OU zoWjjiWKG0;2pHI9&sMY9W-bUI7*HPDbzy+|=(hDg7Yr8mABYKb?Yfrp>OWL}GNTHT z)uuuxuod4gHj@_m+H>Fw*%L`t9QVN)1na(Yj zx2CqnY_{rCp)#8ySLRP>FROA02Dqef_50<07={kxjRYmbO4smz{~H*r=RfeZX7Cmu z9;$-ixRMTr4#PjRE2$E6*jr`zxz^FU^EC`hUG=D8R!Tq=+nsRryTEAl7Il zV_VQOu17`%WMgkX|6jhZ*BLI*)u+aca_$@1n$H^{#l(Fit8_dx6}YNYbiUt8VDI2V z_YLtz`lL)DukF`iO*>#+xuA*(acgn3=;hHSKnk<+ItIVZAMk)Kt}T6H(FD~!R7DbX zVUVDcf~{tfcfBq;pDZL>yIRo@b*jv_xtB_>FQtv_{n5!6G&5HWjI@%^vo~Bvm1GBf z02d$VW>k=F^O`7!vR{b{8ZDaI=&#@EBC6~lV^s2$4o?Ms+=drP)<&rqycRM$dMY6m zXsGnmA7e#ur<4h;YQKgX2Dk)3m(B7(ySqKidHX__VOEMYXM3!6cIzi~Tw1-Gc4g4f zUSI-oN!?zrxjkim1Mmyz_Pb^{O zrjfNV=H(Ac9LbL4s@l)jd{0Rb+8U(t;V%$yi6>y?K3AjIHuqQfdg1iQT&zR#V{7vQ z+}9e%TYy-87rI@M%cZJQZw%{NQi&QFTuE1S%ml7MRI+)VUq(`A+lOj5Ik0WXV*Gtx z3ExXCM0B~>4s?=CObsxT;Q-FpU+?L+05Lm&k5Z15Hfon<_YT&LWpKH}tGLIW^URv9 zs)o$TvNX(jRX@24A{%rXkVzcZ$d`0*lC+N2K>G*X_fx%R8~9(o|BV;KKv$>i<(jWFvw<7kw7!@eV%bA+kvmh4wtYorbu?;2azO&YXZK2iq7l{xA{Z zSzj_qkiOj)ul)lF&?O;R_u3JN>eXMKn39?|`ZA-9^>1M)(LzS+yB0VtA%HO#syB2e zKjM5m`}oP==&$Y()g6_Yh)0*?>Kt`$0PsF01-j+|@cCQn6D2f3PCOUl-hzi+I<`7< zcaBL*gu~9W$}?j#7G{)X9unk2syQCR=wG?kJn))P+HbFA(WQdaE7kt59bWq#GN3CL zLz207u*N4a3MHZ!#W^X{(~1?Lgj7)2got+!vq}Q%HqMQ^zi>FnK^c9yFy2JRO2h@B z1wAz9n}qiQ*Z(?~2LJcPl2?};==K%uq{XPs>s^jO=F}G*RXafPuvpN=mUn==p!6$$ zP*O$?kuJdkeJttCM(C>WE8%93qCT);;`fpHupSdPacYhV}_}vkMvKXu<8FQLy*O2&Kt%^HN&2FnQ17_~%&w zF8BZ3*M8@9PvX(w?Zl*RNz?L;=BoR zzJ?0w#u%JR>F{awN0AihL4Juo<-u#nNOzG!}G^GstxFdswC9xl+kCUb``04hDsWSjkd)&jAm?;bfx}-)jHY4*#tKUdN=j05$))X^gBf|0BfaYS6=n*ZE=+ z^<%9d{)Yn5lY5a&9YQ4EOl z>U3LfsA8db(#NK_HWK81Px?~f5ltTB!me7wr}pKB8Md* zW+XulhkJtE_jMW_F8hjwIIbVyha;chb|6*=a2bGZBfH<_P(*14%54Za*VFwTYJ=5_ zY^_K$eRVC1jj!z?&O})iReZ>#YRTFff z+0DB{m556)MCMC7jS~iRuc4fABqmqPs_ZO)<2*CaMRyPl@OBiROz)gu@HzH4IX~IyD+1}|`Ao9HOiwW+G-abo)H?W&(%5>9atB3%I;lj$ zAtS}aw-bZHI$8iO3(&Jy+URm>wB-{v?G%cWEL~5o+kW-yWO_1KihI zm&ZT^^edJ?)5*VN=%7>KNS4qhFLMK^sX-Gw<>vv;bS-W;S1s6rdeA zfi8h@Xzdd0>X5T@U{WrI2on2GZI)b3MB4XKaN>1P9m?gJwq)$-hW&r0TPT>2OK8ri zas*DbDV%V+D44}e#D4&B@|xf(qIfWF`dKmz;t*ZlX|P#fWcHa$1f7 zr+ra{xnIzx;eTo<-Ev5$nN}GM*mL?fp%2It8ggw?%=|_Z1#rIsT?PK}N#a$NlS8OH zDR~X!b=NSv)fu*eu~Y_}EJT0G;G0<+QX}D6ZqG*tXUWHC7k zx8~03GpK8HNp4*WO^h+C;sBQ)=!(y8eLsVz5Tx&f+gD+1^Vn;u++Qr%m@Kqq!;Wx@ zuNX~6ok4<&dT-!6vc}^Biw@EHaZa*RZ3%_;$7~frlJIutxu{+wV_B=DX8zmHTCfrK5%_P9@A?%?|#U= zW(*v}LtGu9HKWtboag*+9`)Zi|9UOI1?aBky5T2iv(opNU}x!IQTzcVCblal*k#ZX zNhM87xxDF+gDU>~j|ula2@36>mX*x%J6=?;0F?DUWr=zdl-D`rTR-{Qx4i{OOF1TU z%%a0!KgKnN^`gg%$CV{kb9sb&?Mc zZ6?R>PNoW60j>zpZ4-v%u2Icaz@lxu6R3>HL!M3WW#2CPsD=Se`SN#b-ylk;SX;Dp zY|sR**eOZANUo=_|8Iil(Tp(FJRZ&5Hoz4Hx}c>xnD`|6E-sm&?mVXQ}UPHwU<{*YaC{*uL|h?ba(z@m=H39EhcR*u`d69^5Q@OT&I?Y+(52Md@Q{ z7cgmOap(&>CF+U=m7I)Z%Ib`x=_2UKY}+dVye}kxF4XMq(4K1z;wYAdUj&h`8vh;L zs!fE#=8Db6U@3MV%~JHr3n@iiv*XwdD9=$Ytg9@RH4i!lzU7djM2YJDzxvsK*V*fs z{}!O0J-Hu5eJgo7=ygi|D$Tj(x4yF@@!i?&QL#yU{Oc&1$ga9)ESuNY*cH{>iT2@! zsQO;8E;c|57S0u>XGBn7h$6!5LP4ClPZB%qc?ER!f< zlzbwL&q5w~^yiyah73%69FPcBsp|IP z?5=JmE}pj?6Q4sq+<9nW$PsRExBK$W$guFN1m*He5@8f?*q=~E_nBI`stfay7(MmE z-ZX$K4|GkXWrkun!u+q*Vep53nY1AWXTb322a1bSM?vcw4niK{zGG$$T7 ziY7!BAuNYEvb;N2SGg{3Tof+qydxHijTGGE)4vNt5zf5k`1Zdk0$tkd@3kLdpd#9a z@r}?gtgMR=1{ETrH{0}Wzs+zinRQ8|)1?l&@M15|Jui5C3$B+4@ZEP4&WqcfudGU{ zYy{pHN5>yH|ddunsR`N+)eD}g5llUAHq0L z*&u!N{5`fHzJhzBHtExzgxx10&>(Dm`quPie* z=@t@rk#qa20vbmM*_7@;HMCF?z^Hbu2A}^%*KTmfA&UEDmHYc5(e<1XqTnDi0zabS zHD4;gRRy{p^|qV`F^YNTzys8A!dq`nu8}jT%)WeqUJF~rZs2#C~W(3p)T>7t} zaB0E33oIfZ2g6J2>La%ysr-ikt{TwgrayG8vGwcTBuru0ULNGqM>CoOvt+`s&OdaS zra*2+B7IMaO^IlUe{Pr>o;AKhPStJQ@~hbH$Wr^0&VJ5sfU6F4vp)z&+IgVFU&GGq zsJ|CiFU&Yh@rDzI9_J9i*1NWup_smq4|bQ9qx@O^(oh~vsRY*k(_KG&*foi%EVL*v zAK+>L-L+b0s7r^W{BeoIHsxe#uDNH!p>If{!+&+bY^9xaycWja}WVhsx-ayXRk=UgVG-; zc8_?P#aCsjpbk?S;8crQ(h|~PGn#q-So?%rsU&9LE&}~!AK+>MU5GUQ34MBN=^^}~ zp^goEHxR~Ek{zTW`}Z3-Xzd@5akDoCONJ(gih+5fCqVGr>dWr>c|xUExE_5Fk_U2n zxaN{-tboV&sy+Hdxs4z*=LK-T16^4;8}upKngeiN&%thW63c~-PWWFt@czgiE%RkT zo{5G}5y8Yu1z)=nu_6=fI6nM4*=#CFX|2Vd7XEXc)x8?v>I2=L&vpc<8q|oyaf^m+ zy65x$S<_WirPrrBbq_bl1BZPlhLnf-p(Wj{W)?ok#gKzg-UDXL{?PRhI2 z!*XK`#(22j7T}xEhLlg40$f9&`#X}?j5R$w!NmQvv%%T!`*1}y^g2ZPY)Bms_Bf%K zk_{o4Nj`OPk9r#G87Hj(QqO`;NS>+n@o|QjDKkqJaJ(=Ax>A2Mm{R8;IUTXfX;g6s zOq8;PM(C7LzUsi?S*mO=1R;-kHMu}RK@zg0Cm*5sYq{OAsg}LhtSS?aAB>Dm^#tVm zx;Oh4AR^PJ#Hs`xcX}Jes4Yp zf61nkm*I!t=&Xgt>51sZCI-MY0lLRkF+;RB{2D1YgEG_fWOR!dbJBuG3g?(*nNrx!r^;TwqX_Tvqp`P>pGqP=}D+EkOg^*S3BOP-}>*441-{G5eec zjc>C!`wegN31r3R^%!*l1xJ_%gj8!PNpy2CcjDB>=g?oMYQAlP-)pZjrFJNl1Z(eG zUiV7h?u#Elx2LX)Af8WGO8&GS)FUk4IrJB|v#d`t61+?wwSq-$fsLfpba#a9`){ZvisbME8ym zTgMoK+v7IwAiq#B$&oAnOXBlwgF-a9Tmn?pF}T!g~osm$3mm z@td(vTSe~4vpfCYh*j?z)p!kSZ-(3e?rW{*EkG~KUml1{K2zX1IVOMrN5nT<;8_OZEqy6xdpxxfV z-*H40B{BZF`o+W`xv=lHNAI_uyD=4u)a3tb?>*p~IGVq4nNDb?cQBm*a>Za1FsAn& zdX;6_7FIEm+^DAaPUyWPKnMXshY%7<=p91uEp!42Es+0r_U?42awkKc-}C<7|0{nO zbhooJJ3Bi&JG-~Hm#spiZ%UCI{!gB)n^gJIXNE`ZYHy#LT1z^=uuA#fyLbHAKRFAW z*p;zj{XE5fzBs37f#*N0$~@!PjD_3pwAf`X7<8y==&0|rtj=uBx6_#Ob^ZPKZjQ{9 z_xG*4gRfbuW|7V>Y*N0nf+n6CduPVo`o*??Rxvt3yS#+&!0r=Dgbr5?Y@Vt8%I}L9 zcQovA_|TyNg{^~AuN=(gTf$)2{pFM4b9yYzQ*?SS@?nA=5~X}6-y6}bPk+s@X9=el z%*s?NK)dGaQ+2C7S%Lj3Wxv_hWMi+55!;?w?iapQ>Cw*fvop`Ee)&p{Z>rS2`S$z6 z%Th<43y|k?`QJz>(P+4vk5CS@er{V_*4kd8z0KHES@yG^6I^7+UNe`>ixlfX=C1J3(HjS+bgY>UOS_oSE=~k zLtCZm4=GZ4(p?fy-aoNCy8e#QnTMDBs@%A1f4y&2_4fg{ zbN9>_)j#LGb`KtPsbBQeo;GWqXDc`RT;cWWCGw_9`TjC1LY=(yP~|my-!z=s^2~$7 zf3)cQ(C_h);YI!&ba_XFxybM^|M-#MCzfu&o#HJWSJJx^;(CvO;4uGnqTsZ&qgh} zmM|~lkZNO>44QbTV(QB8PW1g}@>jVYFRUuzJ4nj+L&cW&yB@iD;rXA3{o54(Jjmn| zUTN`$5{2)kH8^G&&|>7`v|VSdwcjM`Hl4j!;^xcv$h}#67Fd65-I@cYhU;FOrZGa$ z!(b`jV}+JXOSy0CIdV+Ihymxv8~u-UzI^ky=KC+LF1bEy?@Ui`OssaM-nMK}S-ZV0 z*SuXsoV%1)U!Y~-bh z|EQX+ntA2HiX!E^?`!;M^syEbHIIAepD^Nqt;FbKTQ9eGzh?ZStuw|9q-R2cylGOt zp)>n@Q!nQH*o?0?Z>v)}bp5r(?+)d=KR^4#r*j*{XIyfm$+X&2_tZ}fnAu^*mBZ!Q z-wFSD{FB+qsY?sY2|ZFa`1WZDUy3b-zf9K}&JW6aKvm~e`5EPhKA(B+T84Ioj1!NJ zT~X`7-9e)Zo4_uGeAdspb7=dED+i2mn9$ua zPAGi!vhCQ#s+}_J$+$S_hr)mN4?1`&=igbD%m^AAd*no~@28uKN%)SE@@@P3{`*@V zAKlWeHA{9j?Oitaq1W;-@pn(1`TUyTGRje zlJGxT{FRXB_PCatiXVRRt99zb*Oyw{KM?fv?ZB_*o~Zt!LhCE5Dd;nMY;aZBqwhXN+rk;n|iw z_ru?elg54HrF`$^2tRag;)G&1Za=DdY{!*$jU!I>Xu3_;;$}e9+N+D!Rv4V+oBD%w ziD$~^?{Z+MMsulG;mWTUM3;PdtbLQoBmABQf|p>w)c1tHOrvTYJ$AWn$CW2`-+UB! z=Fff`qDt%6eqo&&R_31ZevjvO!z=myHEhjmpFLS4Ru&5{H7Ivjfr)8F)HmL~I$t+6 zZu4Xb--%Mb+3Gbsa6aJJ(~w0o0z0N%zualmn`tvrTjYwW+GzRy>={<8W)7_Ntf61% zrqUyCe?6d3{LMju(QkK8`Qyp1UoQ4|vaX4Q?<6VT&*MhcpOLn*e(-mpx0j_fs=j5) zk7airO>Nd7Otb&USBn-EDp;s+v$})K*1ij$ez)Lyx>xjR<*(Tvj zvI~Eij@}+LeQwX2IkmBk=QSQyY~OERluO++?PB4WR;z3M(pDhw)N4VZ(sgB=f&egcb+`b4W*b@(C-u}-_VrrQY+4%J9F0RoMXBV8I^GP>b`-C zax^RXHrwLgqi^L&`{HI~;|jO>ojYqQU1;M^J8f_E2M&C*KO*nMAG$AT*je*V!gs2a z@2w#ZHm^IgwE4Sgb5|CQt@$Qjr5*25j=ud}b1`48zk)~QPWnE>+26yD{djWgpwt~@ z8xFnZQ+4XWu`SMz7!_&^FH=tHf2T?L9{A~z|Mr-THAd#EQ10CLymdz$IKTVFnXEm_ z|FkT2!?e}UOf$=+?EkvRyUialH*k(Ye2@R#Xw z#E`I*dDZJJf0toh&5Xs5e|I$K{o6;)^WN08sxiN(t<0gkKTaGF@#0}rQire0ZESvO z{gnIK$Tj^Au53T6p{co_bbOm3<@@y5_uuy3ditkk%^q&<`CGDox3>RQ17t%_vZ_?sVLMub-oLNaJx6A1 zI%5({O*IG`_i}k{&Ap<$ssA+%kWIcUig&(Xiy!a{vrPN1749wQlQlr?Hm8hAl~dw# zm>(*uQWcVJgK9NGU3RUpF)+h8m(qCVz&Z@<4 zxvJb4gv+5KVl8$C=#PhZSVN|f4Dik#d|g2 z)xiId2FTYX>1{fzO{I!#<&6dYht!O>IaR8Q&~N@vm-ctT=)BxFTf<2;RHY5#sL zgZTB;+w`Q1HA6Xmlopq0jZejIl-?x!tc_^?Z~vDKvHt(<)%MEo)qqz6UJZCP z;MIUv16~byHQ?2NR|8%Rcs1bFfL8-v4R|%+)qqz6UJZCP;MIUv16~byHQ?2NR|8%R zcs1bFfL8-v4R|%+)qqz6UJZCP;MIUv16~byHQ?2NR|8%Rcs1bFfL8-v4R|%+)qqz6 zUJZCP;MIUv16~byHQ?2NR|8%Rcs1bFfL8-v4R|%+)qqz6UJZCP;MIUv16~byHQ?2N zR|Efl)Ic`=3v|Bxm)$A zPouIH@@Z7p2>|+A#HZmqIgFOdTFj?W{nr5KkNfsD`>u^@9han9ihQIOd@4!x8LsJX zIZqqOo6#An82&!1ofr}v0c%mP8_G%i$PP%y)Xt<=YDdx~>5240btb)2JxQ-rH_{u) zeijVQ0e%CV0-OerO|=1#9g*F93Frvu1n3Ot0_Y0p2Ivmx0q6@ z1IR|nCdmfL=E%m#rtobf)kW0f65ul6JYXhZDqsR&B483=IP#4Ej09W-j6&KNz*xXA z!UF~YQUIxdB!CfM0+;~_01Mza-~iwdU^`$fU;|(&U>V?d$eJ6!f8+N9;2(ew?%(3~ z9pEM40pKCv5#RdVo5BT7WMAjUn$Q2fGHGurk8o+me`G92r z@;TI9$fryOXaNlYv%zByU@l-DfIg*%V{lbTfMh@lfX@CTzcK(Y5HJWZ7|;sztpRNT z?EvioI73ra2~Y)46;KVJ1_S^C0l|O}Kutgxpcdc@Km>sLcrie6KnXxeKq)|JfFGa? zpe&#qpgdqQbTAsfqX2_(Jp|An;137@1OkEp!GI7zbwDVf2B0P&3=j^e3aAFi3&;n^ z4=4aA2q*+73@8F93Md9B4k!UA2`B|94e$e$0X%~ao&(N6ruw+QfZvONCV-}ZW`O2^ z7JyaAHw?dN06Kqo2%t04Is$S7@&J4Rc>(zV`2ht01p$Qsg#qMaCjuq|rU0e_W&maZ z#sJ0v5&)5adVuj4CyNXD*>wjs{!P%*8;u= ztOFbeoC0J+{_Ftq(-{Htk+u-92(TE?2rv-kl>?LqQ~<;wts|flpbMZYpc|k&pa-BQ zpe%Tm1C$5U0~|)$Zom(Kjeuu>=YT%|F90tA_W_l_KZ<@qvkSkQ0b2lD0owrE0hNK< zf!`CzdlJwd&;dZ>{2ah+z*m5N$nzb37XaFThQ`kz0Hs^_UnBoJUXo5Yg5~`05&W(P zYydm~umKo9Hu~@_{*=bB`p80koBHoWKm))8z<9to0L4A@t!~m0@s7mrWBfh@`~sl5 zBT3a8K>DRVM{Vd2$OAyt*jSefzc~Rp0G|P9Ow0zLF_Om0%z#XQi~zDT;zw;oJgFV2 zJ--ByO|=J5IduTF0pWm}fEs{MKy?7=F%S>{AX(IaYJjSMDga+VVL$;uen37zUO*v0 zK|m2eX+UuRwF~)>qJYW(N-qH*9Hl#_RYV&3E6PVcg?vjXKv_T;fFFST5aB7Eek%Y7 zFQgL>!qGjIMR}+^l0O6x44`^S{ariUw*WK)kZ))Vp#ISiK>DZ;s0WAy)CEwVZ3-ZK zb3hwFYd}jtD?nQS^&9Fx)Q>s?G=OBlK)_dk>3}|f-T-Q=UI5ZJ$=MSiw1=SgX-FFj z&;rH*#sG-tP{3$_888Yk2tZ|w1f&7104g^TFaj_fkN}7R5RDO_14ILew*e3j=nK#T zVgPY~SODcS0f?ss&<{W|P??lQvIym;;aVuyhU+8%@gSNZ03qK1T&Do~15$aQd%_O} zP<|mV)nOPwph@F#q!+3a$wJqY0aF1J0OJ8t8L7@B>l6Uxn*^8$aLz|%5>Q?tkHFvg zcLwrM8s(+(1^i50lMKQ={jLRk2UrRC2Jkgt1zjCQkEdV6@MgZm64EPza8?XyNW&8-(3D^PH4%iCV2B5T`0EGc30jB}K1AYbE z2Mh(A0^9@K2HXT(1e^mL0GtIJ0bB!I0Gt6F0~`XJ2mA&&2-pwU2iObP10XueOMIv- zDo02o8K_L+L%hjmi68NyG7kfe0!ZFMfD-^pKMs)cAsRu>tGFh42v2-TCxoYZUj|T} z2!9DcWm5ht0Ln-CZvd_X2zLuWe5m}p04n1S-~r$ffb>?^2jc|b3qVak4L~R$7!U+_ zjyz8R)UHqPTODa3fIo3fd0yf-3w|kYARqvc89;Oy@k@i>d;B~D{0;aE@B;7$K;TQf zmHtK`FYzND^h;$^xpYrFQUUJ(uK}+Blt0IA}eT!4`nZf-;#h5fZ~8+fTDmR0E#CmuB7;q;!KJ+Dek2BGY5d; zI*RXd0w^w}7&RB5DrBZum3{>%gZo(A`{CCgpaxU}R0UK4d=8-4i(<2ifC_-}fO3F= zpc}-~*2X>A0@*|@Ko}q#;17HXfOJT4>Q?-Y!0%8%8lXR*0iZsB=9tNVM1T!o0rUfy z0VaSE@DJ$Y@Y@2vLjke4js|D|eE_`xH1F&R9L-7F1KI)F0@?st0a^mWfv4EHA%OJV z6u->@6jwI^GzK&RP&(byZ)*V2e+lRS=m_Wp=nSB_Z+Ad9Ko0=Td9{Ej0LezzF#wWZ zhhM@Ikemkm#sl<#z5uFc0)XmIa#;E6B>WCVev&;E*Q7VPCmmB7+1(JpAi!Y2K)?V1 z>2@galul`MPe5`}8eNYB5H0Cs1c2IMIDq&J15jC%kJ@c0&r85b+fXQ*@(8jJKj%Eu z*2GtuJ_h$=0YbY^!Zpc5?K2I((*aWfq!YR)z03fR?XFyb|L|Ar8Tb2H?e4)fdM1Wp zeX@pqohit1eX;`be+DyLR3WWe)sea=U!Pu~{(=61T!fngn5YfI^lSU&&z{YvSD=4T zb^jnX4;+N!z|m8}PmF%GVqX zkn9Vm8iQ5kziX?1{ou-{2vgHP1Q_949)+K1zxd+G=C6SX_Yd-~DSr85?EG_`Gj+b8m5d_w%gh=kc+UeNqv zUA*gR7R1x%=a zFwq!Ot^Ewa7(aSX-uL?9?SjCBpnRalfKxG)lRUSV;atDCVZa3XLpsjS6aeOI*t8bA ze`#D9m~hrS%tICgCV$KiKkV4JwzY`SrRcO8a2(XNYso$@rR+AnlvK`XeaD;Mo;q3)Rz(tkTU4TnRG`{D z^y;dkSHC#p>l0CjGZE?uq_EHFiYDSdn7S4V&M>ndU%^O!oTw`!05 z+O(F4$((`B($ZGU?%v=)+GeeYp*58f;5MOt^MX(3hhGyhwDLum51TKoJGb%4HWAYa zn3BMJ^P%GHtVPc@5iwR^N&&M$-C*?&>)fg$hSmbh0Q3IB-CRBsugwuLnX|GPX4?EA z<#S%y@_in(LKvrO8gCr!ARL_pji4$78j@h&xo$^ZEq?{85-|Co14j!HFq*ze)Nral z+sw!vdAWJL3_gQlhA2v z5NZSC3rxB4i|b_=lQ<~@T077`6kJ*YLp|$JbhnOIYVU4HG=YqrtBnrkh3gC&@;h0Z zv`<>QH<9`e`~+213rucc{9C@6^sw2Jnc!BPldUbvl6nfRoM4!5KhThcENx_ax_d}q z3^07p8VgKOVE*2c$7+oFs$B;7kedGJo~orh&DKn+hC^O_zf{C*2Zq{Ivp?eSm}P^z ziJ0@ikkm;YG3!WwKSKS8&8ep;|noK$aVvZ&eS1Mg9{$&Oq8hzkKRlOhq)zoKTgFv;2Zf7VI}-~GX%yb29u$xtG{@(}*NGeVb~K}5t&kP0sC{x3dfn~W zgzJMiZX{a=U`THjT9sM6x9;Wq3`0Yfss}J+Z;evx&)xBJqg)(K2%4cUFnNJVIzDAd z%Qk0Di{%Ue#uu21uPtMmW$BuQ$3V96z)%nVJ(sqeZ) z9w}nvWqSjHT%dXMeVru*Hg%+Nfm$1#PF`wx+5TG+#F`VL(X^`fb=w`>8aHbWFEw-> zr?KKIv4~6Sj?X@}`SlaPP@6(K@;W#R*Fr6qkdV93)!HE+c)dZ_SAikFlXd>oBjtwQ zMGuB{m?i#>WYXKbIfsh%Gj7enV^Gdtz~lkOzxE#8{c#;S@fdK+48tMJ%nAo|iTOHE z6u@&U08Bn$iYyv1XX45)gBgZdVntwR%qo-X`O-I8O3vmniuI9~O%;njjBKc>d^z$hCoi>3InaSy>t%gTUIKZ$cJ*(t9oE_j8e5z?UYB~x z`qPKA?Q3U;cDNx+6_b~9xWN*F zReRxd$U?gQ^d$5AtB;QD(nB80%R~f!6K?4B1YRQuVu+9s2nuU?{o;x4yto&pOw- z@iz}A->A+o%&uL>IY_{)%F5H^u5@DM%^_y;V&U9~F%TH4&#w0RJsCcno+6er8W{4| z2_27jx!2h;gf9mY%;jmS4qUplWVfhFz)+h)>I99&s#6=Si%ZlmHb__Kxkz&X9gfC& zpVgW@`pKa`07GLvXyogY1OoEP{l31UN~%kK8M*FThCzspH6CC{quq=D zx;Z8E1JUq)G%EsFs_BI>6+c9DA47csGLr-c5k6A;YJ z%)`C|MrE&6W08ot4Gh&MPlLvn^3-`8CSslgL(%QLtiCw{XS~`hV)7w$st(LKUG}j} zayG+y6YM&eqRz&^P|FpJ^}X7*N^@8*NkAC+xK}>9?FDY6gB;h7hTRx$D-LdmI%zuP z8dsql7F)Xd5FtkD2X53$lD=y8a>lg-u!F%gnL!W!lsM6~oWWEOT0uS*TTq(Q&a0M_ zp9bA7M>Y!W)P!Mg0*2;|gLZaE`{L)(S47NlU?6zf!*6vf7M@y!RuE_&15*~5dk3!0 zv~(;}L1=|gRUSX~oGPu|1>KE>M9oZl%2VoNh*B0C$1 zUuuOy`R}%@Jf-Q2>Ob|{bZpaV;`Ci2 zW(AKa_S*P(Rg1FY#ByBwf>6#z(BuWT=9~VxeznK-jv~!IVDbU;@c!Jn9Rl{W1cvtq zzX1am2Tzf2)y?o3Uo%Vy#cz**K@+8Q?Z5oTKMP)6%W)&AbD&#M&#FFf;_N>LZn!F9 zwu@`)24ZbJg*#cHaa08*}3rpoy_N7JX7Hb>d99WOEoSbR_p7>fDc zFlhbH*1%K^cYO-thVVaF&BY> zsiv)XQs(!0<5oNsxCP023U#1+9Z-DW-ct}pe#CH3Lwd^wYot}srrFm$9=dx)c~NS4 z9h3(RoL^dxkl|xD&Hjhx^}HoE2c{Y@=UVsrspwG?jetx$p{kG9fn49l(!Y!j2qUjI zIZTKww;HNXsi~a3VebG8$2GE}R*<7n2mWVi+M}=HiBekQ?ccu&d+{@3F`<3rC6Ke7 zP?`FWCCYP?uTK+9BdBM6cfs$}FVp&C-O_W<`jm3yX|ljyQH->}H1_f7zoMw0Qe1_u zTLc)21`k)i{MwQ-PsT6PP$)NVf2cvvN#W*`kg}(;pa; zt=p4FZ8{wE&CM`)+J@Cho||!bpQWw$oC{*yf(SE@#}xkI-P&#g8qit*U(;`ap?Txf zfNm2jZmJVrfS&(^k=K6-Om1Kn{W4|Q-;a~afrjEYv|0vmB+TlLY2{Wo31DlTw3t>? zCXPR{_L+ccXLTE&P2Mp=0>g0K<@5+8fZOX3&$gh;h*Jn&kPH)|h zwYz@j;lNrvH(25=U?|$S`MvGPi}7)YyoE^Uztsxz<$Ow;%D3Eqwj3|FnyT@&xajsk z=4p*y=KL%Lw!`<}PiZ+kRkTFwY^tlJ-X$fJKM=yWu^HG)ltb&52fvTl9{pu)KVSm6 z$8ho%-~0=1X7nak*-Zr_7eCA3Qwu5NS^u*w_bK-FDLzELrbfLAI6@|I{Yn`ntkb@DfT$}-KoRp1uNtpFMZPnBY--XW3JhD!SI2D*m&9iG_)3w-CTSBgLZSVLuD}RfR+tqBtV-UisZ_v)_ryU4>>z=60mu&+!NeD z=#6_mSAEjD`a8o6G!v&@4{kb}hUs8OY*FoDpKVo`)NIR**`%fAYt?~u_2nvxHJy;;nOHhPX8|7QN<3tcbMsyqJ_Tcg$L z5^Pr0ncZ0%-g~okhRDs9nxIqbttxBpze`SeJ%*l_P&weno;)O43>M#?w*2v?#}r^_ zB^WJ-7fEf2Sa-Nl{Fj#_;(chW=V|uU3u!b;v z6mXu$822t4T=(kvc03J6^P9ks?NmE-_R)7I@;71_ST^3b;4ug5UOcjE%)E`jP(Oui zuX#+XIlF4!`fLKNC-QpBSfAq-)=0NBDr4d948xw6DxV^+mEL+(HxNPSrvIqmiuLnwE8UPdPc)!3+6HN23E^_XU{8o2FABS1TfTt=k2Vz zB_ee_#V)j(hx)VzCND6pFDAV;CF~-5xB&DUEJCzbp0t3$oDLHiKMJnYV)0(I(kCS z$9P&nP8p)X#~XF(=qS~meaoM}-@n4dS}v42x2xY7%(u4dUJr(Wht)LVW)jH}Pj$Nu zQ~cR;8P-0ovN0OSW(Ez}^_26+Rn2Hv-`m5(M5WsB%ok4x z4}|V*wf(LUdc%lC^cy1Y2{~8rMBCapE(JBFG5QLVu-Na#33QvFFy7>e8H| z0Q6D%b2rv=m>)os7v=mNrFvWcpTaZvn!>L40+SDzi<@J=oEp&V1do9P=YS~)%)HN% zw;hbxcN`dc$^)8*z|gp`dBC9GOCQKdG`zii0LB-XOS@VuyFDq6o*D5^hK!n6y%tZ| zWA9aI+NxJE8!*_oNZibvi&rgv)VkC*!#YG<&?s|bR$H`MXG&5<`}J6#FzVjp3_iJA zakg2M|6SCy@X9EC9f~c<-XL{(U~)l%$ZSiie)l?}O$Hwd)=`Wq2pC%5$y>g3{x9eM zeZPPmlcYzlx8cnce!sG>9W$7xiCk z1CtY&nVVZI%T{ymD_{^Ez)pkYYf8Ik&`DJO!*`VLTK0Njz8v_F<={rE9v%BU`seKw z&0oNfRRJTfo$q*>*Aok+6gZgt1>?q^5Zd&nRN4=kFtdEoKaak?2MmqqP|s%2P>d8} z+fm?Tp-Qy2PF*V~R3)!#(i_=VdOai8z z$H0)qzYiGJV#x4{h`5CIc?S&H+l3ERUp!50K3&B4w&rLyuQ?w!VxZq%5mSoC+P1X7V8{~NFFmwndVDjph^Y+>wOr}2;jPb_MqnN+NFZ-JO?YnortYy+tQMPE zz^4jT9DkjG$p>!T*JbOX(~mF|AdNCzJI^r+_~?s<{DwOp1Kzb;yH@#z2&vk@42qNY!I zv%mI_E*~tGcqjzhV7Iqu9+Rg@aCr?qp?kVUM z@?6k$M6V1x&pgd`4HA$?f*-Z&WA&yOvua4!x*3nodNzfxDKIlZL;WX9#8}<5%$4(S zJDJfus^(vEzIX7}MC*aDv)nGx5Q-3F{3u4w1hdtq#akpjG{+7-YME^)%E2Qmf2;;r z%mza=_R8*CUw&v@d%r9hd^&+YS%$o~jIrvHbS9g1>y%-IM?M*IH52XTcDv?Kz()ga zx*A~hwI0`3K;hRdP9qn4s_P2M7<$e!gXF9)wlEC#BL%AZ>TS#tg?D+_ z69J4d{(%O1B?fY}OPKK$Wvuf!UKDWZVS|IFa?HYK#WLRDYZE$If4^?*$Cm z4jMHkS`S`$x3x*wt#v1F(O6#t?E~$|v}uSd2HZmY1`hP=ReeWC&cD64_E~nO_^%jG zQJtV*n8;CJs5R0)>?$0%VpJiqwQmAb0+`i*-rgGfT|Ts_UDlloxj}lBzU`hh>JiPS4%;vz2B29l_Dgg7S z@jnxfZQol@#4G})3^1<@^Iqj>{5{@^5oFs942?Dq3$`ztXHSE2BIYVEWr4}rAYyi| z_92r*3>_+fZ8>R;eq3jsS0sOoh$#mQ(R@4p&iFA;Gqn{ljewyigsMLeFFD@ee0>oU z4Gh(1`>!p|&no;lkBAux3`xDVZSH!DpN$t-{?owMSo`|@;iZ8=o03!P4h+R&SO1xMCAPxxD%kTwZ@9qTVu7J( zP`_+x?ydRqH4-td`v#dM!rmT%rT}Or{&Tz9ow^!$7g|CA!p(ah0*yR3d5lS8GV8IP z*Sk&W@@0QHHy-t2Ys5hc`>Qhc;d*e@v&$?e<2MMqwNTS@z>v?1dowUCQ{7EXz>P)> z^pYFE6b7bdSAER$CI0^G?I>2E*T7J;F(gy_a<8Y3IVocDX}HK?!C+PWo=X?^6)|dH zsI>==-F5r+*4L>bra3T0fjO9^>c|{v$18{!dU=j|*4hB=p0i_&DiJdT7(ZZ&s!w%T zxqaSt5wi#wDn}hxDRb9}nST>8JAt97X3)uhPAB&}9w%ac1EvTty0{Au#@@bqM#MY? zhHBd1+NWivCMkc5825W|D6|_0!N{sycSvT0-pG%-wn7aos1i-ec-xQpAHAK(w*rhu zW;O+k+Zh-e8;zQHtncV8)88jPsD9!h?E&O{{1lXv8|5_Gqtza*^uhuRJ;wkyy!WAw zF=%3^Uyj?JtIMTBB8{6JD)dGo`O(U5cV2}AspKOEH+#&`7fEdAh**qjg5$aN$=Z5A za>D0N>Syr55R5%47}0?t+ljeQcSA>Y*c}n01%@o-z>#AQinZ;9RVjgHI56dbSyl4N zfwLPL!N&_2=l5xSeZE0nvOsuO7DbD1^zH$MG}#^F68TZ<&W_)%&5(yyoN3I0MpJW&sVw2c3#FJz4ts4+nwaTe~gnnqp<) z&2GZb#J;*}7HDV$Ea+DGP1-F!Kf7Z-gtJ$}} zP@BSfn+I>cA54;O{e$c?4VP-!>u*G8-Imt{JT60 zc}$am`IkiXy73J#e48eyaa^j#ru(g2?W`kHM+-ZZ&_2NEje48v&z@~3ytf{z0vejN z!K$WPxN-u%iP(L2WEAZ~Azg!}yp@ys*Ux*WCEtj_oJlALFKs1}MnBvy`|_2o;2efw z@1tY1u-SCcs*{VaomsoN^lDa4FwN&qfE(Gh@$*tqt7k-Em5l^IBdA8&xN_p_%ox+} z^ku3k?O}oh8obC)O?TndFwo8xzhIcs5*}90N$EfJ0a1O zsx?RJK97hRTQhg&o{SsQwTzEPIb~D15#v;jyHNwCtLg0yG#ssz%a1?ueeMpp<%8Z1 z{9b)c;<~Gkz>VHk!K$G=w^RMOIKIW@q2=miebWpy{F;Nzhyfk6h26OKVs0!X1bRcC za~K@JnSN1C?1`9shQ7) zlYK^u5y}=|Xf~DM@~DBc$A(~J5@MHgz?1l0Cjg0brE##6FHj3x4|AU^`i z$D{Hw-@C!wykzQ@J$v+JgRs*=kXpV?iwxmnSW~GNKb<{aK3t@6wO-GWs&lN2IV~^3 z$F%mpk@`OydCS{@yhi0~DsKn!<;dH?f2*c$XCLgrro8R^XJvEq#x;#u@XBu9xTdHR zql~=wm!Hc_9LCK{>JNzQ=wGbPO*SrI+zwQTRGoVX*7^9krW*+$Gp(Gq8P3HA6Mksa zVeH8H+?qE{nN)6NV`H%V2q<5U{McxWHmRdEHchFxH6=ektHxXg!#RsKGWKz^e!z4c z$DdAh-+oG!^k98%-12Xff~8lJ#?ZlL(VJo;{DQ;-til`)uyQ_6sJ0Im&m|oj>enYe z?ib)wFyyM%47++M|LXEDngCDpvaGY-ckT7(cNuN$y)T?c^sc}P%Zw=nqV%Q!O3QO~ zS?wn!`)#*bu>g#7ReGNG8`?gm#p{!pOB4Nwu+SquDY-usc@Bp7GbUzWzc>>Y=WPYa z9k->@M*I2u2iX7lRjla8j=A!S!-h0#fIqvT%??(rMW0}cA&1NG0w%GoWP833`__n$Wr=q8d7C z8_4O8=f3=93SBD*jNFin>PC;Wv2B!OfD#xV%sotrDe9CO)y1S}jU`!ca!f-4o}E}7 z6FFLq#-fdLL+_GqFA55`8pQGh-ZBJyvJ;WWEJ|b5;Z7$q6f2;1MGB>%%5JbCTawY9 zK|#G@u`;y0@@24+MoCv8?!@OyaEJO>_HI2b(U@b*_ zENelnJP#*j3!KHim9AzKRY7*LwRaoRDG0~4?Wp56bmJk_{yX9RsPU})?WoHLZejAMGN6ijvV=h>at@e70&Juuw z&WzYn?c(m~J3E#{Bc4C&ELyYCs4+$BEYbRy7&RO0)Yu}Rjnf&;ef4ULQ#u@qX2$rH zpj9KFGnk__);RxIY~)UeN@W9_9$0Q1SEEFmS!*`f(`m;PVhpv;pri7vE(oIzL5S8h z-J(k{TWo4838KLuN#iF87H2glTC_NBMw4K}dagQNZ!pkU9Ie)y`s%bc?ixGOZECGS zk0KTH>5><^bV);{DUQY1mPl%{lCl+eikZMnynzV@(TSK~qd;t9XRnbI>;gkOoz16A z!=2;?8twu#w3SYohC3GyG~5LV$5oLFc<7u3y;hBF06L43{@mmSPdBM30MV%jp@faa zU?E(R!C{p~xibB%py+2+<4FT83?Q>rAFWek4-Oqb)jEp>VV*gP z%nMd#O~9{1X402K0*uq)K3S)UM_M{%QS;fIW#W^WGFc1bcnw4nW>{IQjwGXS-ikAl z6k<$?W~d7}7lEX7wUwhXz{N3v)JMx}$_?irG!p8F&Xo9j(Ko7acK4fI+8nYNLlS*gI=TRmbTJNLO;a zG8B+m1|znSLSr~o0=yjVNrfK!vE4X9ZNC;Zr%<-5RyY<;s$%u<)N!2@4n|HXtS+oA zl_iq4A}5YCg>msDi^N92L2{Qa8SJJ8TKl!AgJ|Z@d1I9CWYTP4B)t`RDQJRD_9FrC zgZ%v9qmUo9#O^b$y0~CY)6)nbx(?8ZH%vZH{no*?k?#Wwmo&!7Q=3Z%ZZMJFs@Wp1 z2M@yb#t?2$#$(LOiHP8fbx5dW-Ru!Vx&+M3WvtkO|`l z_UbFxr9(b&bGT=5iYNP^c#lqtq-yY&YWCRELp5BYOowO^+>|q@*}20XTLvG=y5r8H zK8>#sI=f>m;69GCQ?M>&?1Vxq(XMypaU7>`f(8CZ+k|+GSGn*I6AUU=SYK0fn+>R- zD2oQ+J`AeJ$k6zt0>C-^y~Fdq`inBpKZycHYGO>mTWI%Fi44hhWqJatybDH3=% zCV&TZC1ssTr(ED5y@j`;ty;?T^!A1ZgzT5i4H-qG6=4r1M`jC_LIaFC3zjP}7u0Et zSP?Z_5nm-}l1()9aj9{Mn0%?R#Tu){R_t3yFlexVhfx{VTn={de=1J@1nq6~u7D(F#;1SL2zNtcu35Bdc)e?{|6t{X#t zY-q!z)0&t-%NDw51;N~92U~Z^%`@TTDJK{@ zrLZu9i6kJ{5>Yx>vV0PT>>dLbA5|!D5XOEG3zwjy2@;&32|HWV>}X*B01Cmxdj*ax zKiIKfRwYm2A3GEn6zo@2fN^pZOoJUx#n&g*;N;he-`b~aur zRLj0#2nP0RHT&>{r|6P>%7Vz~lNK19>76L$VGJ8ekstrVjnL^Dl${;I%ytXv`Xy=% z7W|LZPIU|xJ84Y9Y9kvhxs^k;&7#3$Ijho=oxAKP)m?JBW;Jdk0y{UU=`uX0>VlI~ z3hN4<**kE!wjOOS>y#B_9L$gWw zk&9@Es2+RJf*Mg=0u{5((}C(J92nvs%ooN=cMx2#L7UEqxXPvzGV+dzzoiYD@L?Jw zOxFaDg$Uff#t_-pqQ??%vd&Ds$5rqT>1_sp(4cBT+ zdK_DX%@Z~q=Jr~fg&NF^hZz{%^od3m+*p9Y)&#_w=nguyLwV6r3WB;PfXMCj3GImES3g`~Y$>=Hmb}WUjC@;Rq-!WZMdxaL^ISUap zmKj>IIy=R1#ASKmnK8M+*Sq}2oz+Mr9sgSJlLvwh)eMl^FVWM82MUh)Y%FFR5r8lJ3E=v+ekxV!!Bw^dLcb zxH8i7Fr$?QH;;Lx7GjT|L2kbW7kuf(Ly4|&Ias(RvS1^fo{VcNstJFUKC6P2(?@wg z#$T~$LYRn9HSjo;1rv&~1TAocf(iSD*nF_3U>HJIaGp(bqm4MB0#kk-2k*qsxSjK) z&oCT&h;(vjSDM3=@uRr9$S6)7-PJ(EN=k7Q5L=GS*z&Ez-YIngJ`CrmFn-P}@^K{A zVqF@fS_3v-sbg?WUzX!*=a|zPkLN2)wQxp=OlUa+dFgqXnw+evWUI*?5w?Si_j*%8qD{OtC6Xr; z@98Z3WV1y=fKz7KDxaOBRY&jfh$MKa1`AvK3skgDldk%5RH!a@!4@W1^er-`4I|an9K9S_ANxxiU+hbZ6kb6Cd`!N4&Ab1Z(Lj-~Sx6DF&Ird{$1 ze5LeEJl5RO?C?M)I{G4-Q%E`saiVf6&I!v3_B1}(n*_Y<*D#iJJYKW+5^6(`(+`_L zq~FZA{smg@f|cuOz9kwN-(5k@U!iB=`$Hb)LL$B30Kh=F1P81)dYl5;tIqipFyJm& zFZ&2`=Al5%em}Ns&fS5UyZC5w<~@m=T}a494e9u|ZPi>J1uK=MVe)bB?DAYh8^O}N zDTzHWP~a?PB1PFL&|@*`QCnkOpKY=7tL?`;fmIWOfHx{J20sdXrP;t)dJ73XZ`s05 z0Qw9KNck&9#n!JBN)`2ln$U1Y^xPHOrTP)ZBaDzt0&cKGBl3c?HF=%YNc&3s@tUrJ zNVJX#9`vJTzM7eTUyU{%uZw8n*|rs{Hql~9#p7oky*-83543SOv@-#D=&fP4-GFUk zpkft@a!-K5-6PoAfG3SQ3k7b@N$`ZUVL*9RMp92E5Vr4fwPyC}X|;^DgOU>gsc?y~ z$n*M+{c!~d?bp-{!Q7CkpiFxUfz*Bt55%v9MCz>?jn+zDJyEZ~#vvbAvHWcNnkP4C zOttni1XGRK1+*N@55yY06>Y_7^p={;^A2^1sGyhJ z!CuMIlQoaKeSjCfnCF2PEQUTA1P1&Sqw+NV(kFG@~37xCSgre^?i8Y5rv>dG52c@b559uxB z;2TrHc212o#hR*MQ6QJxp>)qLM+hI^ z0jY3_QawLZ7mXj3;*B^FQqU~R9!>EGe}xk1t3DnYRMLw;D!F57X3c}mUFeH8Gj%C* zj19*Rr{lS}G#eO8Zy}iH-QkXff`elMYeY|VqwleRl73iiJmqI!dIACaO&0}yH3|gu z1BpDpi6(JhM8-yKZfhai8sed?>`szs1&5g~Iho&scLkwv33)vaxl#`bTInsy^=#0B z<0I1nz4Vq@fv43yiRTBoP8S6wf5mjgY#Kq7Uc&|nrJzZY3rr+;5Z?1V zU-U31ODTIpyRKY>`YT8U98!rr+uRe^kOEk z5O30@Z|;+ZpyMu>IkJ+uc#NJdv1t(=8L@rBO2+^=W&}^i1g!CTcH?5Ppcil0vWh3$ z;bH^O@j-%peLz8X^f@U|&=2bip2lPPJSFne5Au7i5BG^HP;eKlO+1x>HB;*A^n>=` z_a6u^le1UV6e{G9k0j+ze!#>OFM+Y;HBW}@=14Fmjs1Wy#)VZPGD>UK1tf)G3l4|H z$FFdL7(HObGJwVul}M}U>=%nXQTA*OiQMb0_9X7s%AMS+wrKr>=z~rcyt)@H5O z3S(CsUC_cwR4RAcG5Z1sy}K01uGs0>$#JRBhnm~giNjH712iv)(X7Q;Yr-A>vK`z9 z3K0^yv)6<>PB7ulYQkY1ab|m7YeH(I4!f=Cz!^LnM%x%8ac%^r5_E(Oj%=fS^1aw3 zos*9~3c`sRQ74$pIWUgC$)?69nuIchEx(wnrzTtU*y<%<67|BBJ~q}SP-Nk~rC zMMYcmFcH03!E^;?tZ`v->{o1|bWl3z4;&os)5S?*YoM3hv9ZXL2NG(4W&kz*Pgj|C zD!ws^l)p?DJ-JzOO6-@79iCPoq$UkQ=`HOw!=sS&Itk7k!GrHGG^gNP1ND0xJr&KL zf&^{ow8FuI_`;MzYhpE(o+i)_Rzy9AM-r|A8O_I;5_lZ{#uaT&!P^P6(}bOiqacJ+ zPB3;#VT$&sCUPKFMD92qUe}MpR^bvYOHZ7XV+y?M;;KY2AziZJD!m5D`4CWY7wIx9 z!7Mo!1zPSxq@}0mm=-4EeK^G>JBNJW<8aR+NoiAYScR71o0Q8ZvZFZG)1G!Wju;Hw zq@r>8mz~i9cnS?rkjS0~%pi|Dv3W>u_JX|E#<63X!^SHjD<7-^VHF%<;q9W8MSj&srJhb42TsY-SuzMBmR+(~hBNM9d0x z7QO+7!i7twG*7Na94|pB-mpL!M_?)U2m9~~TKhHYNuE|b=>uCNDt_Qo_~l|!F&4W$ zgbczL(m*a;vM`R4*l?WgmpV@-)lXm+ls3wtaM=%TFA?Ceq(B!8SC-ZcboohC{j7b91K z#$tqsZbcA|S)?4wfJSQoK$5nB3ri0Q0T6fvG%G;^Io8P~pR3j(72nr+}!2{Eq zRvtXCs~J1=FEA90*dZO#*@vNzF2T8vOA2vD{i||Z+>N9l7u>%p$?jgsx$`I8@*{#X z>R**3b-G;c9Z(WC;*jnzSSmEUJdtw?IB=A-M|zn{Bz0yjL45*a zv4#1O&!J9^*iUB9+XCle-4r0=|4k91aX72UZd+oOe^U_WvSkagdvu}GrMHo#eZ!f% z6!!_TJ4a^D?49wSTm|0^K)`+id(jlRVbqNoiDT&}Y90)n<#rh2(!o~ZV8Kdq zhvgW4I;3C@v;srNEP_>Fo-xfJ|2P$w7jQ0(6eQAJP`xAaETCsmZp{I zoF9dO-Z=>)w0*XR^PIR17)sBG?F{HK9QTM#%2rHe>#91 z;%}L?c%pZGbP6u6iL7^evON2d5MW`yrsK6d8?DPDU9gZm;AO>os*3PH#_2gW)}ECO zpGDDZzasykDwOS#M$t0A@1HBjfq+|Qbs^$*3t8L{a9_j{UQ~7^bV#A+9o**zxu>V( z?YJvWxBQ11{c5x0rYZauJ`$$TeZ&$Rs~bw6BPNXzA5Ki=X94^N!N^%*?6OgNlPd=Y z{sUp?$wnhi`?RGBSvYm^`9&FLoCd+y5i(D7H0l{WAwdnBjEN2yLJ zDBJTUpmf<8c&95L^T3|MAM7^aU+Uqn*r3R~2@>#97hx6jEM|gO;!V0jFG&X@VzIlW zOXJ)(fy0p@!wLXg856aCQ346aRy&n=g8;^;SN7;utUt% z{StMFO0OGBa)F8Djy1R^v7KEtSU4xKC)qqS@DHxC<2SIk-tkG^J8-iV6sq=vMr@Z3<;^vLS>sVn;sTi@?KO zG7FSEDOJA0;3|(~gS4k^=q}C%R~BzG;XL^eCk9SoJXkrUq$_OUnDP?^@L)S;5vSGQ zUWK)z>0!D;LpbCE8}{rSY|``4-g)i%2YUy4HZG=9PRWFqpVERGe+5^7{coN|cD@P{M!fqL$6Yq|uv@U?P0Zb1 z@Q6BWGq^(i(?7aVP~uSs;=+QkoZ^r|iy2UdOPXYX#b(7<2K8!gmB1k*Rhr-F=9(@Y zh}|r?;3MkX;(;7rekUFKE~_5zhs)1@-EemzMh%@(B=!3^&W>`E>KrQa>3rE9VZJ3k znTu5EkWG4P zv>U$cwL#hn%3Vl=__(S$hyZpD_n*vFtTnZOT?L=aPw-NldlFYp73%6d0jDvB4KWlB zBX+mhaFDcz{RBcbQZr8hx9$10Qu`;>Kxn^4Ku-JZlm{lKkEem&DJ5MKBggNh5fjG` zsL`~TjTm@d63`d#(9>s{J!~JQY~aLl)7eCx!UufYES}yTVZUrbz}{k1Xfaywz&X~S z;jh#|^if8SAt2WqSwKMo(s#0H_QB{}!aJi`qacF2?BHoMgA6T?*g5^uFj=^V6`ph@ z1Q^)o4l4YQ-~-bIMkc`_8rLa~BD^NVKo8u+8!+`eX|NZE(3E7s4++ChtCdv87f#ai hL$YuQ&DHQ|`Nn7y`wj&P6)s`x2*H@fRsT!>{y%HSw`c$W literal 119304 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

kU{jf)gJd2J>o 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 mysql.createPool({ - host: process.env.MYSQL_HOST, - port: Number(process.env.MYSQL_PORT), - user: process.env.MYSQL_USER, - password: process.env.MYSQL_PASSWORD, + ...connectionParams, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, }); + +export type DatabaseStatus = "unhealthy" | "ok"; +export const getDatabaseStatus = async (): DatabaseStatus => { + let status: DatabaseStatus; + + try { + const connection = await mysql.createConnection({ + ...connectionParams, + connectTimeout: 1000, + }); + connection.connect(); + connection.end(); + + status = "ok"; + } catch (e) { + console.error(e.code); + status = "unhealthy"; + } + + return status; +}; diff --git a/app/server/services/ts/common/types.ts b/app/server/services/ts/common/types.ts index c27a677..e8097de 100644 --- a/app/server/services/ts/common/types.ts +++ b/app/server/services/ts/common/types.ts @@ -1,5 +1,7 @@ import type { RowDataPacket } from "mysql2"; +export type DatabaseStatus = "unhealthy" | "ok"; + export type User = { id: number; username: string; diff --git a/app/server/services/ts/user/integration/setup.ts b/app/server/services/ts/user/integration/setup.ts index de537fa..d30dc30 100644 --- a/app/server/services/ts/user/integration/setup.ts +++ b/app/server/services/ts/user/integration/setup.ts @@ -105,7 +105,7 @@ export async function createGoal( goalData: GoalData, ) { const response = await request(baseUrl) - .post("/goals") + .post("/api/goals") .set("Authorization", `Bearer ${token}`) .send(goalData); diff --git a/app/server/services/ts/user/src/DataConversion.ts b/app/server/services/ts/user/src/DataConversion.ts index b5a35f9..cd5fdff 100644 --- a/app/server/services/ts/user/src/DataConversion.ts +++ b/app/server/services/ts/user/src/DataConversion.ts @@ -1,4 +1,3 @@ - //C0j export function convertToDateTime(date: string) { return date.slice(0, 19).replace("T", " "); diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index 451824c..f968850 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -1,5 +1,5 @@ import express from "express"; -import { pool } from "./db.ts"; +import { getConnectionPool, getDatabaseStatus } from "@/sqlUtil"; import { authenticateJWT } from "./utils/auth.ts"; import { generateDateRange } from "./utils/dates.ts"; import { validatePeriod } from "./validation.ts"; @@ -24,9 +24,9 @@ import { transactionsRouter } from "./routes/transaction.js"; import { debtRouter } from "./routes/debt.ts"; import { savingRouter } from "./routes/saving.ts"; +const pool = getConnectionPool(); const app = express(); app.use(buildCorsConfig()); -onExit(async () => await server.close()); app.use(express.json()); @@ -45,20 +45,11 @@ app.use("/savings", savingRouter); const server = app.listen(PORT, () => { console.log(`User Service running on port ${PORT}`); }); -process.on("SIGTERM", () => cleanup); - - async function cleanup() { - try{ - server.close() - await pool.end() - } catch(error){ - console.log(error) - } -} - -app.get("/charts/expenses", async (req, res) => { -process.on("SIGTERM", () => server.close()); -}) +onExit(async () => { + await new Promise((res) => server.close(res)); + await pool.end(); + process.exit(0); +}); app.get( "/charts/expenses", @@ -307,4 +298,9 @@ app.delete("/goals", async (req: express.Request, res: express.Response) => { } }); +app.get("/health", async (_, res) => { + const status = await getDatabaseStatus(); + res.send(status); +}); + export { generateDateRange }; //for testing purposes only diff --git a/app/server/services/ts/user/src/logic/goals.ts b/app/server/services/ts/user/src/logic/goals.ts index 5fd2f97..fac6085 100644 --- a/app/server/services/ts/user/src/logic/goals.ts +++ b/app/server/services/ts/user/src/logic/goals.ts @@ -169,7 +169,7 @@ export async function createUserGoal( const newGoal = await goalsQueries.createGoal(pool, profileId, goalData); if (!newGoal) { - throw new Error("Failed to create goal"); + throw new Error("Failed to create goal."); } const enrichedGoal = await enrichGoalWithProgress(pool, newGoal, profileId); diff --git a/hooks/format_ts_dir.sh b/hooks/format_ts_dir.sh index a449c26..18fbed8 100755 --- a/hooks/format_ts_dir.sh +++ b/hooks/format_ts_dir.sh @@ -1,12 +1,13 @@ #!/bin/bash #first argument ($1) is the directory to format the files of. -changed_files=$(git diff --cached --name-only --diff-filter=ACMRd | grep -E $1/) +changed_files=$(git diff --cached --name-only --diff-filter=ACMRd | grep -E $1/ | grep -E '\.json|\.ts|\.js^') if [ -z "$changed_files" ]; then exit 0 fi normalized_files=$(echo $changed_files | sed "s|$1/||g" ) cd $1 +echo $normalized_files bun eslint $normalized_files if [[ $? != 0 ]]; then echo "❌ Failed to format files" @@ -14,5 +15,5 @@ if [[ $? != 0 ]]; then fi bun prettier $normalized_files --write --ignore-unknown -git update-index --again +git update-index --again --ignore-missing From efe5d6a73fa7071dada0616c011d13c4de63cb68 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:39:16 -0500 Subject: [PATCH 192/204] Feature/content delivery (#92) * setup docker compose file for prod deployment * add deployment compose file for webserver + re-arranged + workflow tags images with latest created script for EC2 env setup & webhook * Make upgrade-images.sh executable * add sudo to docker commands * added readme.md * make upgrade-images.sh executable * start docker daemon in start.sh * make everything use constants to get ready for AWS hosting * localhost -> api_gateway_url in config * added HTTP security group for webserver * finishing touches * script to remove all workflow runs - useful to get rid of troubleshooting runs * disabling db reliant integration tests * temp * removed node modules in server directory * Changed release version to local so ci-pipeline pulls uses newly built images for test base images. * add PATH back to CORS allowed methods --------- Co-authored-by: Roman Tebel --- .github/workflows/delete_all_workflow_runs.sh | 11 + .github/workflows/docker_compose_publish.yml | 8 + app/client/.env | 2 +- app/client/docker-compose.yml | 4 +- app/client/src/App.tsx | 7 +- app/client/src/api/Account.ts | 3 +- app/client/src/api/AuthAPI.ts | 4 +- app/client/src/api/Income.ts | 3 +- app/client/src/api/Market.ts | 5 +- app/client/src/api/Transaction.ts | 3 +- app/client/src/utils/constants.ts | 8 +- .../services/python/analytics/src/main.py | 10 +- app/server/services/ts/auth/src/index.ts | 2 +- app/server/services/ts/common/expressUtils.ts | 19 +- app/server/services/ts/user/bun.lock | 1133 ---- app/server/services/ts/user/package-lock.json | 4554 ----------------- deployment/README.md | 4 + deployment/{ => infrastructure}/.gitignore | 0 .../{ => infrastructure}/.terraform.lock.hcl | 0 deployment/infrastructure/main.tf | 121 + .../modules/finus/main.tf | 10 +- deployment/{ => infrastructure}/terraform.tf | 0 deployment/infrastructure/terraform.tfstate | 1492 ++++++ .../infrastructure/terraform.tfstate.backup | 1486 ++++++ deployment/main.tf | 79 - deployment/scripts/install-deps.sh | 30 + deployment/scripts/start.sh | 4 + deployment/scripts/upgrade-images.sh | 3 + deployment/scripts/upgrade-on-new-image.js | 60 + deployment/services/.env | 3 + deployment/services/default_container.env | 9 + deployment/services/docker-compose.yml | 36 + deployment/services/secrets.env | 5 + deployment/webserver/.env | 3 + deployment/webserver/docker-compose.yml | 5 + 35 files changed, 3325 insertions(+), 5801 deletions(-) create mode 100755 .github/workflows/delete_all_workflow_runs.sh delete mode 100644 app/server/services/ts/user/bun.lock delete mode 100644 app/server/services/ts/user/package-lock.json create mode 100644 deployment/README.md rename deployment/{ => infrastructure}/.gitignore (100%) rename deployment/{ => infrastructure}/.terraform.lock.hcl (100%) create mode 100644 deployment/infrastructure/main.tf rename deployment/{ => infrastructure}/modules/finus/main.tf (82%) rename deployment/{ => infrastructure}/terraform.tf (100%) create mode 100644 deployment/infrastructure/terraform.tfstate create mode 100644 deployment/infrastructure/terraform.tfstate.backup delete mode 100644 deployment/main.tf create mode 100755 deployment/scripts/install-deps.sh create mode 100755 deployment/scripts/start.sh create mode 100755 deployment/scripts/upgrade-images.sh create mode 100644 deployment/scripts/upgrade-on-new-image.js create mode 100644 deployment/services/.env create mode 100644 deployment/services/default_container.env create mode 100644 deployment/services/docker-compose.yml create mode 100644 deployment/services/secrets.env create mode 100644 deployment/webserver/.env create mode 100644 deployment/webserver/docker-compose.yml diff --git a/.github/workflows/delete_all_workflow_runs.sh b/.github/workflows/delete_all_workflow_runs.sh new file mode 100755 index 0000000..8dd8e80 --- /dev/null +++ b/.github/workflows/delete_all_workflow_runs.sh @@ -0,0 +1,11 @@ +REPO="MeasureOneCodeTwice/Finus" + +# Optional: count runs first +gh api --paginate "/repos/$REPO/actions/runs?per_page=100" --jq '.workflow_runs[].id' | wc -l + +# Delete everything +gh api --paginate "/repos/$REPO/actions/runs?per_page=100" --jq '.workflow_runs[].id' \ +| while read -r RUN_ID; do + echo "Deleting run $RUN_ID" + gh api -X DELETE "/repos/$REPO/actions/runs/$RUN_ID" >/dev/null + done diff --git a/.github/workflows/docker_compose_publish.yml b/.github/workflows/docker_compose_publish.yml index 7e43184..c2193a5 100644 --- a/.github/workflows/docker_compose_publish.yml +++ b/.github/workflows/docker_compose_publish.yml @@ -36,6 +36,14 @@ jobs: docker compose build echo "Pushing images..." docker compose push + + echo RELEASE_VERSION=latest > .env + echo DOCKER_HUB_USERNAME=$DOCKER_HUB_USERNAME >> .env + echo "Re-building with lastest tag..." + docker compose build + echo "Pushing images..." + docker compose push + env: DOCKER_HUB_USERNAME: ${{ inputs.DOCKER_HUB_USERNAME }} diff --git a/app/client/.env b/app/client/.env index f955b13..e0ce3dd 100644 --- a/app/client/.env +++ b/app/client/.env @@ -3,4 +3,4 @@ #docker-file DOCKER_HUB_USERNAME=measureonecodetwice -RELEASE_VERSION=0.0.1 +RELEASE_VERSION=latest diff --git a/app/client/docker-compose.yml b/app/client/docker-compose.yml index 8c95c65..d995f0b 100644 --- a/app/client/docker-compose.yml +++ b/app/client/docker-compose.yml @@ -6,9 +6,9 @@ services: webserver: image: $DOCKER_HUB_USERNAME/finus-webserver:$RELEASE_VERSION - build: + build: dockerfile: Dockerfile ports: - "80:80" - depends_on: + depends_on: - webserver-base diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index d1ef33e..9892c57 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -13,10 +13,7 @@ import DashboardPage from "./pages/DashboardPage.tsx"; import MarketsPage from "./pages/MarketsPage"; import AppLayout from "./components/AppLayout.tsx"; import ProjectionPage from "./pages/ProjectionPage.tsx"; - -const SESSION_STORAGE_KEY = "finus-session"; -const API_BASE_URL = - import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000"; +import { BASE_URL, SESSION_STORAGE_KEY } from "@/utils/constants"; function isValidAuthUser(value: unknown): value is AuthUser { if (!value || typeof value !== "object") { @@ -104,7 +101,7 @@ async function requestAuth( payload: Record, ): Promise { try { - const response = await fetch(`${API_BASE_URL}${path}`, { + const response = await fetch(`${BASE_URL}${path}`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index e1deda3..df93723 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,8 +1,9 @@ import type { updateResponse } from "../types/responseTypes"; import type { Account } from "../types/AccountType"; import type { AuthSession } from "@/types/authTypes"; +import { BASE_URL } from "@/utils/constants"; -const requestUrl = "http://localhost:3000/api/accounts"; +const requestUrl = `${BASE_URL}/api/accounts`; //Sends a request to get different accounts the user has export async function getUserAccounts( diff --git a/app/client/src/api/AuthAPI.ts b/app/client/src/api/AuthAPI.ts index 36f2c27..09a3652 100644 --- a/app/client/src/api/AuthAPI.ts +++ b/app/client/src/api/AuthAPI.ts @@ -1,12 +1,12 @@ import type { AuthApiResponse } from "@/types/authTypes"; -import { API_BASE_URL } from "@/utils/constants"; +import { BASE_URL } from "@/utils/constants"; export async function requestAuth( path: string, payload: Record, ): Promise { try { - const response = await fetch(`${API_BASE_URL}${path}`, { + const response = await fetch(`${BASE_URL}${path}`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts index 7d66c89..dff2958 100644 --- a/app/client/src/api/Income.ts +++ b/app/client/src/api/Income.ts @@ -1,8 +1,9 @@ import type { updateResponse } from "../types/responseTypes"; import { type Income } from "../types/IncomeType"; import type { AuthSession } from "@/types/authTypes"; +import { BASE_URL } from "@/utils/constants"; -const requestUrl = "http://localhost:3000/api/income"; +const requestUrl = `${BASE_URL}/api/income`; export async function getIncome(session: AuthSession): Promise { try { diff --git a/app/client/src/api/Market.ts b/app/client/src/api/Market.ts index e2a98dd..df430cb 100644 --- a/app/client/src/api/Market.ts +++ b/app/client/src/api/Market.ts @@ -19,10 +19,7 @@ type MarketHistoryResponse = { points: MarketHistoryPoint[]; }; -function toMarketApiError( - error: unknown, - fallbackMessage: string, -): Error { +function toMarketApiError(error: unknown, fallbackMessage: string): Error { if (axios.isAxiosError(error)) { const detail = error.response?.data?.detail; if (typeof detail === "string" && detail.length > 0) { diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 3153898..2eed076 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -2,8 +2,9 @@ import type { AuthSession } from "@/types/authTypes"; import type { updateResponse } from "../types/responseTypes"; import type { Transaction } from "../types/Transaction"; import type { TransactionDraft } from "@/utils/ConvertTransaction"; +import { BASE_URL } from "@/utils/constants"; -const requestUrl = "http://localhost:3000/api/transactions"; +const requestUrl = `${BASE_URL}/api/transactions`; //Sends a GET request to get the list of user transactions for the account export async function getTransactions( diff --git a/app/client/src/utils/constants.ts b/app/client/src/utils/constants.ts index 08d5070..bb3cdfa 100644 --- a/app/client/src/utils/constants.ts +++ b/app/client/src/utils/constants.ts @@ -1,7 +1,3 @@ -export const BASE_URL = "http://localhost:3000"; //change this to the public link once microservices are hosted on AWS - -export const SIDEBAR_WIDTH = 260; - +export const BASE_URL = "http://3.150.23.72:3000"; export const SESSION_STORAGE_KEY = "finus-session"; -export const API_BASE_URL = - import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000"; +export const SIDEBAR_WIDTH = 260; diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py index c0ada55..a57c199 100644 --- a/app/server/services/python/analytics/src/main.py +++ b/app/server/services/python/analytics/src/main.py @@ -23,7 +23,15 @@ app = FastAPI() # CORS setup -origins = ["http://localhost:8080", "http://localhost:3000", "http://127.0.0.1:8080", "http://127.0.0.1:3000"] +origins = [ + "http://localhost:8080", + "http://localhost:3000", + "http://127.0.0.1:8080", + "http://127.0.0.1:3000", + "http://18.190.215.135", #prod webserver + "http://3.142.125.202", #dev webserver +] + app.add_middleware( CORSMiddleware, allow_origins=origins, diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts index 1e664ad..a80bb3c 100644 --- a/app/server/services/ts/auth/src/index.ts +++ b/app/server/services/ts/auth/src/index.ts @@ -9,8 +9,8 @@ import { getConnectionPool, getDatabaseStatus } from "@/sqlUtil"; const pool = getConnectionPool(); const app = express(); -app.use(express.json()); app.use(buildCorsConfig()); +app.use(express.json()); app.post("/signup", async (req, res) => { let body: SignupBody; diff --git a/app/server/services/ts/common/expressUtils.ts b/app/server/services/ts/common/expressUtils.ts index 70a6c31..f4e313c 100644 --- a/app/server/services/ts/common/expressUtils.ts +++ b/app/server/services/ts/common/expressUtils.ts @@ -1,13 +1,16 @@ import cors from "cors"; -const defaultMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]; -const defaultOrigins = ["http://localhost", "http://localhost:8080"]; -export function buildCorsConfig(opts?: { - origins?: string | string[]; - methods?: string[]; -}) { +const defaultMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]; +const origins = [ + "http://localhost", + "http://localhost:8080", + "http://18.190.215.135", //prod webserver + "http://3.142.125.202", //dev webserver +]; + +export function buildCorsConfig(opts?: { methods?: string[] }) { return cors({ - origin: opts?.origin ?? defaultOrigins, + origin: origins, methods: opts?.methods ?? defaultMethods, credentials: true, allowedHeaders: ["Content-Type", "Authorization"], @@ -21,7 +24,7 @@ export async function handleServerError( ) { await func().catch((e) => res.status(500).json({ - message: message ?? e.message, + message: e.message ?? message, }), ); } diff --git a/app/server/services/ts/user/bun.lock b/app/server/services/ts/user/bun.lock deleted file mode 100644 index 21f8076..0000000 --- a/app/server/services/ts/user/bun.lock +++ /dev/null @@ -1,1133 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "user-service", - "dependencies": { - "@mysql/xdevapi": "^8.0.35", - "axios": "^1.13.6", - "express": "^5.2.1", - "jsonwebtoken": "^9.0.3", - "mysql2": "^3.15.3", - "stryker-mutator-bun-runner": "^0.4.0", - "supertest": "^7.2.2", - "vitest": "^4.0.18", - }, - "devDependencies": { - "@eslint/js": "^9.39.2", - "@types/bun": "latest", - "@types/express": "^5.0.6", - "@types/node": "^25.1.0", - "@typescript-eslint/eslint-plugin": "^8.54.0", - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^10.1.0", - "eslint-plugin-react": "^7.37.5", - "globals": "^17.3.0", - "jiti": "^2.6.1", - "supertest": "^7.2.2", - "typescript-eslint": "^8.54.0", - }, - "peerDependencies": { - "typescript": "^5.9.3", - }, - }, - }, - "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], - - "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="], - - "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], - - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], - - "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="], - - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], - - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], - - "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], - - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="], - - "@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="], - - "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], - - "@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="], - - "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="], - - "@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="], - - "@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], - - "@inquirer/editor": ["@inquirer/editor@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/external-editor": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA=="], - - "@inquirer/expand": ["@inquirer/expand@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ=="], - - "@inquirer/external-editor": ["@inquirer/external-editor@2.0.4", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA=="], - - "@inquirer/figures": ["@inquirer/figures@2.0.4", "", {}, "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ=="], - - "@inquirer/input": ["@inquirer/input@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ=="], - - "@inquirer/number": ["@inquirer/number@4.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA=="], - - "@inquirer/password": ["@inquirer/password@5.0.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A=="], - - "@inquirer/prompts": ["@inquirer/prompts@8.3.2", "", { "dependencies": { "@inquirer/checkbox": "^5.1.2", "@inquirer/confirm": "^6.0.10", "@inquirer/editor": "^5.0.10", "@inquirer/expand": "^5.0.10", "@inquirer/input": "^5.0.10", "@inquirer/number": "^4.0.10", "@inquirer/password": "^5.0.10", "@inquirer/rawlist": "^5.2.6", "@inquirer/search": "^4.1.6", "@inquirer/select": "^5.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w=="], - - "@inquirer/rawlist": ["@inquirer/rawlist@5.2.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w=="], - - "@inquirer/search": ["@inquirer/search@4.1.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ=="], - - "@inquirer/select": ["@inquirer/select@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA=="], - - "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], - - "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@mysql/xdevapi": ["@mysql/xdevapi@8.0.35", "", { "dependencies": { "google-protobuf": "3.19.4", "lossless-json": "2.0.1", "parsimmon": "1.18.1" } }, "sha512-l7HoBt1l0GwdCBXBrqri4kPBbJrqsyvaa4eqXvq50aWWArOTc5XGV06FWF1epxT+RORBy7scZTWFANxpr4Sfuw=="], - - "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/8IzqSu4/OWGRs7Fs2ROzGVwJMFTBQkgAp6sAthkBYoN7OiM4rY/CpPVs2X9w9N1W61CHSkEdNKi8HrLZKfK3g=="], - - "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-TT7eUihnAzxM2tlZesusuC75PAOYKvUBgVU/Nm/lakZ/DpyuqhNkzUfcxSgmmK9IjVWzMmezLIGZl16XGCGJng=="], - - "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-CYjIHWaQG7T4phfjErHr6BiXRs0K/9DqMeiohJmuYSBF+H2m56vFslOenLCguGYQL9jeiiCZBeoVCpwjxZrMgQ=="], - - "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-8XMLyRNxHF4jfLajkWt+F8UDxsWbzysyxQVMZKUXwoeGvaxB0rVd07r3YbgDtG8U6khhRFM3oaGp+CQ0whwmdA=="], - - "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jBwYCLG5Eb+PqtFrc3Wp2WMYlw1Id75gUcsdP+ApCOpf5oQhHxkFWCjZmcDoioDmEhMWAiM3wtwSrTlPg+sI6Q=="], - - "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-z3GFCk1UBzDOOiEBHL32lVP7Edi26BhOjKb6bIc0nRyabbRiyON4++GR0zmd/H5zM5S0+UcXFgCGnD+b8avTLw=="], - - "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-KZlf1jKtf4jai8xiQv/0XRjxVVhHnw/HtUKtLdOeQpTOQ1fQFhLoz2FGGtVRd0LVa/yiRbSz9HlWIzWlmJClng=="], - - "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-ADImD4yCHNpqZu718E2chWcCaAHvua90yhmpzzV6fF4zOhwkGGbPCgUWmKyJ83uz+DXaPdYxX0ttDvtolrzx3Q=="], - - "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-J+qz4Al05PrNIOdj7xsWVTyx0c/gjUauG5nKV3Rrx0Q+5JO+1pPVlnfNmWbOF9pKG4f3IGad8KXJUfGMORld+Q=="], - - "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UOdkwScHRkGPz+n9ZJU7sTkTvqV7rD1SLCLaru1xH8WRsV7tDorPqNCzEN1msOIiPRK825nvAtEm9UsomO1GsA=="], - - "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-E51tyWDP1l0CbjZYhiUxhDGPaY8Hf5YBREx0PHBff1LM1/q3qsJ6ZvRUa8YbbOO0Ax9QP6GHjD9vf3n6bXZ7QA=="], - - "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-cCsXK9AQ9Zf18QlVnbrFu2IKfr4sf2sfbErkF2jfCzyCO9Bnhl0KRx63zlN+Ni1xU7gcBLAssgcui5R400N2eA=="], - - "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], - - "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@stryker-mutator/api": ["@stryker-mutator/api@9.6.0", "", { "dependencies": { "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-kJEEwOVoWDXGEIXuM+9efT6LSJ7nyxnQQvjEoKg8GSZXbDUjfD0tqA0aBD06U1SzQLKCM7ffjgPffr154MHZKw=="], - - "@stryker-mutator/core": ["@stryker-mutator/core@9.6.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/instrumenter": "9.6.0", "@stryker-mutator/util": "9.6.0", "ajv": "~8.18.0", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.2.4", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.7.2", "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.2.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-oSbw01l6HXHt0iW9x5fQj7yHGGT8ZjCkXSkI7Bsu0juO7Q6vRMXk7XcvKpCBgRgzKXi1osg8+iIzj7acHuxepQ=="], - - "@stryker-mutator/instrumenter": ["@stryker-mutator/instrumenter@9.6.0", "", { "dependencies": { "@babel/core": "~7.29.0", "@babel/generator": "~7.29.0", "@babel/parser": "~7.29.0", "@babel/plugin-proposal-decorators": "~7.29.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.28.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "angular-html-parser": "~10.4.0", "semver": "~7.7.0", "tslib": "2.8.1", "weapon-regex": "~1.3.2" } }, "sha512-tWdRYfm9LF4Go7cNOos0xEIOEnN7ZOSj38rfXvGZS9IINlvYBrBCl2xcz/67v6l5A7xksMWWByZRIq2bgdnnUg=="], - - "@stryker-mutator/util": ["@stryker-mutator/util@9.6.0", "", {}, "sha512-gw7fJOFNHEj9inAEOodD9RrrMEMhZmWJ46Ww/kDJAXlSsBBmdwCzeomNLngmLTvgp14z7Tfq85DHYwvmNMdOxA=="], - - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], - - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], - - "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], - - "@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="], - - "@vitest/runner": ["@vitest/runner@4.1.1", "", { "dependencies": { "@vitest/utils": "4.1.1", "pathe": "^2.0.3" } }, "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg=="], - - "@vitest/spy": ["@vitest/spy@4.1.1", "", {}, "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA=="], - - "@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - - "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="], - - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - - "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], - - "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - - "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - - "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], - - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], - - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - - "bun": ["bun@1.3.11", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.11", "@oven/bun-darwin-x64": "1.3.11", "@oven/bun-darwin-x64-baseline": "1.3.11", "@oven/bun-linux-aarch64": "1.3.11", "@oven/bun-linux-aarch64-musl": "1.3.11", "@oven/bun-linux-x64": "1.3.11", "@oven/bun-linux-x64-baseline": "1.3.11", "@oven/bun-linux-x64-musl": "1.3.11", "@oven/bun-linux-x64-musl-baseline": "1.3.11", "@oven/bun-windows-aarch64": "1.3.11", "@oven/bun-windows-x64": "1.3.11", "@oven/bun-windows-x64-baseline": "1.3.11" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-AvXWYFO6j/ZQ7bhGm4X6eilq2JHsDVC90ZM32k2B7/srhC2gs3Sdki1QTbwrdRCo8o7eT+167vcB1yzOvPdbjA=="], - - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], - - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], - - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - - "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], - - "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], - - "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], - - "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="], - - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], - - "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], - - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@10.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA=="], - - "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], - - "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], - - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - - "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], - - "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="], - - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - - "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - - "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], - - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "google-protobuf": ["google-protobuf@3.19.4", "", {}, "sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - - "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - - "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - - "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], - - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], - - "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], - - "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], - - "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], - - "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], - - "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], - - "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], - - "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], - - "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "lossless-json": ["lossless-json@2.0.1", "", {}, "sha512-KW/FSL426qblKVvf4ImeMVGr0Je6J9aXvAMUOIU8AzelDj06q47mn6QJ+56lBBd+A8kjrncrxdKQs6ZssAXTmw=="], - - "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], - - "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], - - "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], - - "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "mutation-server-protocol": ["mutation-server-protocol@0.4.1", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g=="], - - "mutation-testing-elements": ["mutation-testing-elements@3.7.2", "", {}, "sha512-i7X2Q4X5eYon72W2QQ9HND7plVhQcqTnv+Xc3KeYslRZSJ4WYJoal8LFdbWm7dKWLNE0rYkCUrvboasWzF3MMA=="], - - "mutation-testing-metrics": ["mutation-testing-metrics@3.7.2", "", { "dependencies": { "mutation-testing-report-schema": "3.7.2" } }, "sha512-ichXZSC4FeJbcVHYOWzWUhNuTJGogc0WiQol8lqEBrBSp+ADl3fmcZMqrx0ogInEUiImn+A8JyTk6uh9vd25TQ=="], - - "mutation-testing-report-schema": ["mutation-testing-report-schema@3.7.2", "", {}, "sha512-fN5M61SDzIOeJyatMOhGPLDOFz5BQIjTNPjo4PcHIEUWrejO4i4B5PFuQ/2l43709hEsTxeiXX00H73WERKcDw=="], - - "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], - - "mysql2": ["mysql2@3.18.2", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw=="], - - "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - - "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], - - "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], - - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "parsimmon": ["parsimmon@1.18.1", "", {}, "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], - - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], - - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], - - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], - - "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], - - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - - "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], - - "stryker-mutator-bun-runner": ["stryker-mutator-bun-runner@0.4.0", "", { "dependencies": { "@stryker-mutator/api": "^9.0.1", "@stryker-mutator/util": "^9.0.1", "execa": "^9.6.0", "glob": "^11.0.3", "semver": "^7.5.4" }, "peerDependencies": { "@stryker-mutator/core": "^9.0.0", "bun": ">=1.0.0" } }, "sha512-wx2nfdfKFWRxE5fiAYzRBVJoUMtoeYIE2uAtWS1fu4rGeay6XVm3wBmqi2bKosVH6zHPShsciSCDKDGifZHvSA=="], - - "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], - - "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - - "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - - "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="], - - "typed-rest-client": ["typed-rest-client@2.2.0", "", { "dependencies": { "des.js": "^1.1.0", "js-md4": "^0.3.2", "qs": "^6.14.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-/e2Rk9g20N0r44kaQLb3v6QGuryOD8SPb53t43Y5kqXXA+SqWuU7zLiMxetw61jNn/JFrxTdr5nPDhGY/eTNhQ=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], - - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - - "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], - - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - - "vitest": ["vitest@4.1.1", "", { "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", "@vitest/pretty-format": "4.1.1", "@vitest/runner": "4.1.1", "@vitest/snapshot": "4.1.1", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.1", "@vitest/browser-preview": "4.1.1", "@vitest/browser-webdriverio": "4.1.1", "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA=="], - - "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@stryker-mutator/core/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - - "@stryker-mutator/core/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "@stryker-mutator/instrumenter/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "stryker-mutator-bun-runner/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "@stryker-mutator/core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - } -} diff --git a/app/server/services/ts/user/package-lock.json b/app/server/services/ts/user/package-lock.json deleted file mode 100644 index 3f9019d..0000000 --- a/app/server/services/ts/user/package-lock.json +++ /dev/null @@ -1,4554 +0,0 @@ -{ - "name": "user-service", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "user-service", - "dependencies": { - "@mysql/xdevapi": "^8.0.35", - "axios": "^1.13.6", - "express": "^5.2.1", - "jsonwebtoken": "^9.0.3", - "mysql2": "^3.15.3", - "vitest": "^4.1.1" - }, - "devDependencies": { - "@eslint/js": "^9.39.2", - "@types/bun": "latest", - "@types/express": "^5.0.6", - "@types/node": "^25.1.0", - "@types/supertest": "^7.2.0", - "@typescript-eslint/eslint-plugin": "^8.54.0", - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^9.39.2", - "eslint-plugin-react": "^7.37.5", - "globals": "^17.3.0", - "jiti": "^2.6.1", - "supertest": "^7.2.2", - "typescript-eslint": "^8.54.0" - }, - "peerDependencies": { - "typescript": "^5.9.3" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "license": "MIT" - }, - "node_modules/@mysql/xdevapi": { - "version": "8.0.35", - "license": "GPL-2.0", - "dependencies": { - "google-protobuf": "3.19.4", - "lossless-json": "2.0.1", - "parsimmon": "1.18.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bun": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.11.tgz", - "integrity": "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bun-types": "1.3.11" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "5.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.3.0", - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } - }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", - "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion/node_modules/balanced-match": { - "version": "4.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", - "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.1", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.1", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.1", - "@vitest/utils": "4.1.1", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.1", - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.1", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/axios": { - "version": "1.13.6", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.2", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "license": "BSD-3-Clause" - }, - "node_modules/bun-types": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", - "integrity": "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/denque": { - "version": "2.1.0", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-abstract": { - "version": "1.24.1", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.4", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formidable": { - "version": "3.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generate-function": { - "version": "2.3.1", - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/google-protobuf": { - "version": "3.19.4", - "license": "BSD-3-Clause" - }, - "node_modules/gopd": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/is-property": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "devOptional": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.7.4", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "license": "Apache-2.0" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lossless-json": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/lru.min": { - "version": "1.1.4", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/mysql2": { - "version": "3.18.2", - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.2", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.2", - "long": "^5.3.2", - "lru.min": "^1.1.4", - "named-placeholders": "^1.1.6", - "sql-escaper": "^1.3.3" - }, - "engines": { - "node": ">= 8.0" - }, - "peerDependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parsimmon": { - "version": "1.18.1", - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.0", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "dev": true, - "license": "MIT" - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "2.0.0-next.6", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sql-escaper": { - "version": "1.3.3", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=2.0.0", - "node": ">=12.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/superagent": { - "version": "10.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.1", - "cookiejar": "^2.1.4", - "debug": "^4.3.7", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.5", - "formidable": "^3.5.4", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.14.1" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supertest": { - "version": "7.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cookie-signature": "^1.2.2", - "methods": "^1.1.2", - "superagent": "^10.3.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.56.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.1", - "@vitest/mocker": "4.1.1", - "@vitest/pretty-format": "4.1.1", - "@vitest/runner": "4.1.1", - "@vitest/snapshot": "4.1.1", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.1", - "@vitest/browser-preview": "4.1.1", - "@vitest/browser-webdriverio": "4.1.1", - "@vitest/ui": "4.1.1", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..9ed169f --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,4 @@ +The `services` and `webserver` folders contain all the files to deploy Finus microservices and webserver respectively. +To run a service +1. `cp scripts/*` to either `services` or `webserver` +2. in whichever folder you chose run `./start.sh` diff --git a/deployment/.gitignore b/deployment/infrastructure/.gitignore similarity index 100% rename from deployment/.gitignore rename to deployment/infrastructure/.gitignore diff --git a/deployment/.terraform.lock.hcl b/deployment/infrastructure/.terraform.lock.hcl similarity index 100% rename from deployment/.terraform.lock.hcl rename to deployment/infrastructure/.terraform.lock.hcl diff --git a/deployment/infrastructure/main.tf b/deployment/infrastructure/main.tf new file mode 100644 index 0000000..01ada4e --- /dev/null +++ b/deployment/infrastructure/main.tf @@ -0,0 +1,121 @@ +provider "aws" { + region = "us-east-2" +} + +resource "aws_vpc" "finus" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "Finus" + } +} + +resource "aws_subnet" "prod" { + vpc_id = aws_vpc.finus.id + cidr_block = "10.0.0.0/17" +} + +resource "aws_subnet" "dev" { + vpc_id = aws_vpc.finus.id + cidr_block = "10.0.128.0/17" +} + +resource "aws_internet_gateway" "default" { + vpc_id = aws_vpc.finus.id +} + +resource "aws_security_group" "webserver" { + name = "finus-webserver" + vpc_id = aws_vpc.finus.id + + ingress { + description = "HTTP from anywhere" + cidr_blocks = ["0.0.0.0/0"] + from_port = "80" + to_port = "80" + protocol = "tcp" + } +} + +resource "aws_security_group" "services" { + name = "finus-services" + vpc_id = aws_vpc.finus.id + + ingress { + description = "TCP 3000 from anywhere" + cidr_blocks = ["0.0.0.0/0"] + from_port = "3000" + to_port = "3000" + protocol = "tcp" + } +} + + + +resource "aws_security_group" "default" { + name = "finus-default" + vpc_id = aws_vpc.finus.id + + ingress { + description = "SSH from anywhere" + cidr_blocks = ["0.0.0.0/0"] + from_port = "22" + to_port = "22" + protocol = "tcp" + } + + ingress { + description = "Docker webhook" + cidr_blocks = ["0.0.0.0/0"] + from_port = "4444" + to_port = "4444" + protocol = "tcp" + } + + egress { + description = "All outbound traffic" + from_port = "0" + to_port = "0" + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } +} + +resource "aws_route_table" "route_table" { + vpc_id = aws_vpc.finus.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.default.id + } +} + +module "prod" { + source = "./modules/finus" + + environment_name = "prod" + ssh_key_pair_name = "finus-dev" + security_group_ids = [aws_security_group.default.id] + webserver_security_group_ids = [aws_security_group.webserver.id] + backend_security_group_ids = [aws_security_group.services.id] + subnet_id = aws_subnet.prod.id + vpc_id = aws_vpc.finus.id + gateway_id = aws_internet_gateway.default.id + route_table_id = aws_route_table.route_table.id +} + + +module "dev" { + source = "./modules/finus" + + environment_name = "dev" + ssh_key_pair_name = "finus-dev" + security_group_ids = [aws_security_group.default.id] + webserver_security_group_ids = [aws_security_group.webserver.id] + backend_security_group_ids = [aws_security_group.services.id] + subnet_id = aws_subnet.dev.id + vpc_id = aws_vpc.finus.id + gateway_id = aws_internet_gateway.default.id + route_table_id = aws_route_table.route_table.id +} diff --git a/deployment/modules/finus/main.tf b/deployment/infrastructure/modules/finus/main.tf similarity index 82% rename from deployment/modules/finus/main.tf rename to deployment/infrastructure/modules/finus/main.tf index cf683eb..6ad2b7d 100644 --- a/deployment/modules/finus/main.tf +++ b/deployment/infrastructure/modules/finus/main.tf @@ -7,6 +7,12 @@ variable "ssh_key_pair_name" { variable "security_group_ids" { type = list(string) } +variable "webserver_security_group_ids" { + type = list(string) +} +variable "backend_security_group_ids" { + type = list(string) +} variable "subnet_id" { type = string } @@ -34,7 +40,7 @@ resource "aws_instance" "backend" { ami = data.aws_ami.amazon_linux.id subnet_id = var.subnet_id key_name = var.ssh_key_pair_name - vpc_security_group_ids = var.security_group_ids + vpc_security_group_ids = concat(var.security_group_ids, var.backend_security_group_ids) tags = { Name = join("", ["finus-", var.environment_name, "-backend"]) @@ -46,7 +52,7 @@ resource "aws_instance" "webserver" { ami = data.aws_ami.amazon_linux.id subnet_id = var.subnet_id key_name = var.ssh_key_pair_name - vpc_security_group_ids = var.security_group_ids + vpc_security_group_ids = concat(var.security_group_ids, var.webserver_security_group_ids) tags = { Name = join("", ["finus-", var.environment_name, "-webserver"]) diff --git a/deployment/terraform.tf b/deployment/infrastructure/terraform.tf similarity index 100% rename from deployment/terraform.tf rename to deployment/infrastructure/terraform.tf diff --git a/deployment/infrastructure/terraform.tfstate b/deployment/infrastructure/terraform.tfstate new file mode 100644 index 0000000..8a641c7 --- /dev/null +++ b/deployment/infrastructure/terraform.tfstate @@ -0,0 +1,1492 @@ +{ + "version": 4, + "terraform_version": "1.14.7", + "serial": 122, + "lineage": "ae3e9505-c9b9-9084-580e-4ba9a7f0ba2f", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "aws_internet_gateway", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:internet-gateway/igw-0a74c1fef454437c8", + "id": "igw-0a74c1fef454437c8", + "owner_id": "177559564758", + "region": "us-east-2", + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_route_table", + "name": "route_table", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:route-table/rtb-0f24beedbed7e02a3", + "id": "rtb-0f24beedbed7e02a3", + "owner_id": "177559564758", + "propagating_vgws": [], + "region": "us-east-2", + "route": [ + { + "carrier_gateway_id": "", + "cidr_block": "0.0.0.0/0", + "core_network_arn": "", + "destination_prefix_list_id": "", + "egress_only_gateway_id": "", + "gateway_id": "igw-0a74c1fef454437c8", + "ipv6_cidr_block": "", + "local_gateway_id": "", + "nat_gateway_id": "", + "network_interface_id": "", + "transit_gateway_id": "", + "vpc_endpoint_id": "", + "vpc_peering_connection_id": "" + } + ], + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "rtb-0f24beedbed7e02a3", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_internet_gateway.default", + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0c03eb59bd6270fcf", + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "All outbound traffic", + "from_port": 0, + "ipv6_cidr_blocks": [ + "::/0" + ], + "prefix_list_ids": [], + "protocol": "-1", + "security_groups": [], + "self": false, + "to_port": 0 + } + ], + "id": "sg-0c03eb59bd6270fcf", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "Docker webhook", + "from_port": 4444, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 4444 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "SSH from anywhere", + "from_port": 22, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 22 + } + ], + "name": "finus-default", + "name_prefix": "", + "owner_id": "177559564758", + "region": "us-east-2", + "revoke_rules_on_delete": false, + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "sg-0c03eb59bd6270fcf", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "services", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0e428a46b850e036a", + "description": "Managed by Terraform", + "egress": [], + "id": "sg-0e428a46b850e036a", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "TCP 3000 from anywhere", + "from_port": 3000, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 3000 + } + ], + "name": "finus-services", + "name_prefix": "", + "owner_id": "177559564758", + "region": "us-east-2", + "revoke_rules_on_delete": false, + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "sg-0e428a46b850e036a", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0973f01390a9d839a", + "description": "Managed by Terraform", + "egress": [], + "id": "sg-0973f01390a9d839a", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "HTTP from anywhere", + "from_port": 80, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 80 + } + ], + "name": "finus-webserver", + "name_prefix": "", + "owner_id": "177559564758", + "region": "us-east-2", + "revoke_rules_on_delete": false, + "tags": null, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "sg-0973f01390a9d839a", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_subnet", + "name": "dev", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-09d5718b3434ec2be", + "assign_ipv6_address_on_creation": false, + "availability_zone": "us-east-2c", + "availability_zone_id": "use2-az3", + "cidr_block": "10.0.128.0/17", + "customer_owned_ipv4_pool": "", + "enable_dns64": false, + "enable_lni_at_device_index": 0, + "enable_resource_name_dns_a_record_on_launch": false, + "enable_resource_name_dns_aaaa_record_on_launch": false, + "id": "subnet-09d5718b3434ec2be", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_cidr_block": "", + "ipv6_cidr_block_association_id": "", + "ipv6_ipam_pool_id": null, + "ipv6_native": false, + "ipv6_netmask_length": null, + "map_customer_owned_ip_on_launch": false, + "map_public_ip_on_launch": false, + "outpost_arn": "", + "owner_id": "177559564758", + "private_dns_hostname_type_on_launch": "ip-name", + "region": "us-east-2", + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "subnet-09d5718b3434ec2be", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_subnet", + "name": "prod", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-05e5676ab26e11acc", + "assign_ipv6_address_on_creation": false, + "availability_zone": "us-east-2c", + "availability_zone_id": "use2-az3", + "cidr_block": "10.0.0.0/17", + "customer_owned_ipv4_pool": "", + "enable_dns64": false, + "enable_lni_at_device_index": 0, + "enable_resource_name_dns_a_record_on_launch": false, + "enable_resource_name_dns_aaaa_record_on_launch": false, + "id": "subnet-05e5676ab26e11acc", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_cidr_block": "", + "ipv6_cidr_block_association_id": "", + "ipv6_ipam_pool_id": null, + "ipv6_native": false, + "ipv6_netmask_length": null, + "map_customer_owned_ip_on_launch": false, + "map_public_ip_on_launch": false, + "outpost_arn": "", + "owner_id": "177559564758", + "private_dns_hostname_type_on_launch": "ip-name", + "region": "us-east-2", + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "subnet-05e5676ab26e11acc", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_vpc", + "name": "finus", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:vpc/vpc-0e13d5c774ed2b3c8", + "assign_generated_ipv6_cidr_block": false, + "cidr_block": "10.0.0.0/16", + "default_network_acl_id": "acl-0f7a72d6813ae088c", + "default_route_table_id": "rtb-09da1959a912933f3", + "default_security_group_id": "sg-06987cbe4f87d0594", + "dhcp_options_id": "dopt-01d5dfa449859477f", + "enable_dns_hostnames": false, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "id": "vpc-0e13d5c774ed2b3c8", + "instance_tenancy": "default", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "ipv6_cidr_block_network_border_group": "", + "ipv6_ipam_pool_id": "", + "ipv6_netmask_length": 0, + "main_route_table_id": "rtb-09da1959a912933f3", + "owner_id": "177559564758", + "region": "us-east-2", + "tags": { + "Name": "Finus" + }, + "tags_all": { + "Name": "Finus" + } + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "vpc-0e13d5c774ed2b3c8", + "region": "us-east-2" + }, + "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==" + } + ] + }, + { + "module": "module.dev", + "mode": "data", + "type": "aws_ami", + "name": "amazon_linux", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-2::image/ami-0b0b78dcacbab728f", + "block_device_mappings": [ + { + "device_name": "/dev/xvda", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "3000", + "snapshot_id": "snap-0afc5230d5113aa86", + "throughput": "125", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp3" + }, + "no_device": "", + "virtual_name": "" + } + ], + "boot_mode": "uefi-preferred", + "creation_date": "2026-03-03T18:01:43.000Z", + "deprecation_time": "2026-06-01T23:47:00.000Z", + "description": "Amazon Linux 2023 AMI 2023.10.20260302.1 x86_64 HVM kernel-6.1", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "image-id", + "values": [ + "ami-0b0b78dcacbab728f" + ] + } + ], + "hypervisor": "xen", + "id": "ami-0b0b78dcacbab728f", + "image_id": "ami-0b0b78dcacbab728f", + "image_location": "amazon/al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "v2.0", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": false, + "name": "al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "name_regex": null, + "owner_id": "137112412989", + "owners": null, + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-2", + "root_device_name": "/dev/xvda", + "root_device_type": "ebs", + "root_snapshot_id": "snap-0afc5230d5113aa86", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_eip", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-04f46a5b83e1a6af0", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-04f46a5b83e1a6af0", + "associate_with_private_ip": null, + "association_id": "eipassoc-0818aa83d85c2f91b", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-04f46a5b83e1a6af0", + "instance": "i-008147727278f7080", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-0f364cdcae030e0e6", + "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", + "private_ip": "10.0.221.90", + "ptr_record": "", + "public_dns": "ec2-16-58-118-74.us-east-2.compute.amazonaws.com", + "public_ip": "16.58.118.74", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-dev-backend" + }, + "tags_all": { + "Name": "finus-dev-backend" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.aws_instance.backend", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_eip", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-0857b784b20a92dad", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0857b784b20a92dad", + "associate_with_private_ip": null, + "association_id": "eipassoc-06e143e0fc9004868", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-0857b784b20a92dad", + "instance": "i-020ba441d6c3c0258", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-03283cda15cf431d3", + "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", + "private_ip": "10.0.140.152", + "ptr_record": "", + "public_dns": "ec2-3-142-125-202.us-east-2.compute.amazonaws.com", + "public_ip": "3.142.125.202", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-dev-webserver" + }, + "tags_all": { + "Name": "finus-dev-webserver" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.aws_instance.webserver", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_instance", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-008147727278f7080", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-008147727278f7080", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0f364cdcae030e0e6" + } + ], + "primary_network_interface_id": "eni-0f364cdcae030e0e6", + "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.221.90", + "public_dns": "", + "public_ip": "16.58.118.74", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-0c0b46050d4eb986e", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-09d5718b3434ec2be", + "tags": { + "Name": "finus-dev-backend" + }, + "tags_all": { + "Name": "finus-dev-backend" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0c03eb59bd6270fcf", + "sg-0e428a46b850e036a" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-008147727278f7080", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_instance", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-020ba441d6c3c0258", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-020ba441d6c3c0258", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-03283cda15cf431d3" + } + ], + "primary_network_interface_id": "eni-03283cda15cf431d3", + "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.140.152", + "public_dns": "", + "public_ip": "3.142.125.202", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-09e9f25279b1c2ac1", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-09d5718b3434ec2be", + "tags": { + "Name": "finus-dev-webserver" + }, + "tags_all": { + "Name": "finus-dev-webserver" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0973f01390a9d839a", + "sg-0c03eb59bd6270fcf" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-020ba441d6c3c0258", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_route_table_association", + "name": "assoc", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "gateway_id": "", + "id": "rtbassoc-04712335fcc923e95", + "region": "us-east-2", + "route_table_id": "rtb-0f24beedbed7e02a3", + "subnet_id": "subnet-09d5718b3434ec2be", + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_internet_gateway.default", + "aws_route_table.route_table", + "aws_subnet.dev", + "aws_vpc.finus" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "data", + "type": "aws_ami", + "name": "amazon_linux", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-2::image/ami-0b0b78dcacbab728f", + "block_device_mappings": [ + { + "device_name": "/dev/xvda", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "3000", + "snapshot_id": "snap-0afc5230d5113aa86", + "throughput": "125", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp3" + }, + "no_device": "", + "virtual_name": "" + } + ], + "boot_mode": "uefi-preferred", + "creation_date": "2026-03-03T18:01:43.000Z", + "deprecation_time": "2026-06-01T23:47:00.000Z", + "description": "Amazon Linux 2023 AMI 2023.10.20260302.1 x86_64 HVM kernel-6.1", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "image-id", + "values": [ + "ami-0b0b78dcacbab728f" + ] + } + ], + "hypervisor": "xen", + "id": "ami-0b0b78dcacbab728f", + "image_id": "ami-0b0b78dcacbab728f", + "image_location": "amazon/al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "v2.0", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": false, + "name": "al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "name_regex": null, + "owner_id": "137112412989", + "owners": null, + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-2", + "root_device_name": "/dev/xvda", + "root_device_type": "ebs", + "root_snapshot_id": "snap-0afc5230d5113aa86", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_eip", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-0262f07d6e23594c4", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0262f07d6e23594c4", + "associate_with_private_ip": null, + "association_id": "eipassoc-0202a50414bdbee94", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-0262f07d6e23594c4", + "instance": "i-0dd805bcaa0a0ade3", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-0d9461ec08710f2d7", + "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", + "private_ip": "10.0.50.231", + "ptr_record": "", + "public_dns": "ec2-3-150-23-72.us-east-2.compute.amazonaws.com", + "public_ip": "3.150.23.72", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-prod-backend" + }, + "tags_all": { + "Name": "finus-prod-backend" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.aws_instance.backend", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_eip", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-06fe359af4fbd5f9b", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-06fe359af4fbd5f9b", + "associate_with_private_ip": null, + "association_id": "eipassoc-0ce3ab9543feb5d7f", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-06fe359af4fbd5f9b", + "instance": "i-0f897d2768cad0033", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-0e2de615da26dd0d9", + "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", + "private_ip": "10.0.57.91", + "ptr_record": "", + "public_dns": "ec2-18-190-215-135.us-east-2.compute.amazonaws.com", + "public_ip": "18.190.215.135", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-prod-webserver" + }, + "tags_all": { + "Name": "finus-prod-webserver" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.aws_instance.webserver", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_instance", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0dd805bcaa0a0ade3", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0dd805bcaa0a0ade3", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0d9461ec08710f2d7" + } + ], + "primary_network_interface_id": "eni-0d9461ec08710f2d7", + "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.50.231", + "public_dns": "", + "public_ip": "3.150.23.72", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-0115ad31723e63439", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-05e5676ab26e11acc", + "tags": { + "Name": "finus-prod-backend" + }, + "tags_all": { + "Name": "finus-prod-backend" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0c03eb59bd6270fcf", + "sg-0e428a46b850e036a" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-0dd805bcaa0a0ade3", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_instance", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0f897d2768cad0033", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0f897d2768cad0033", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0e2de615da26dd0d9" + } + ], + "primary_network_interface_id": "eni-0e2de615da26dd0d9", + "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.57.91", + "public_dns": "", + "public_ip": "18.190.215.135", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-05d61c7cb18058c36", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-05e5676ab26e11acc", + "tags": { + "Name": "finus-prod-webserver" + }, + "tags_all": { + "Name": "finus-prod-webserver" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0973f01390a9d839a", + "sg-0c03eb59bd6270fcf" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-0f897d2768cad0033", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_route_table_association", + "name": "assoc", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "gateway_id": "", + "id": "rtbassoc-0b1e2801d2830d94c", + "region": "us-east-2", + "route_table_id": "rtb-0f24beedbed7e02a3", + "subnet_id": "subnet-05e5676ab26e11acc", + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_internet_gateway.default", + "aws_route_table.route_table", + "aws_subnet.prod", + "aws_vpc.finus" + ] + } + ] + } + ], + "check_results": null +} diff --git a/deployment/infrastructure/terraform.tfstate.backup b/deployment/infrastructure/terraform.tfstate.backup new file mode 100644 index 0000000..cdbe6d6 --- /dev/null +++ b/deployment/infrastructure/terraform.tfstate.backup @@ -0,0 +1,1486 @@ +{ + "version": 4, + "terraform_version": "1.14.7", + "serial": 117, + "lineage": "ae3e9505-c9b9-9084-580e-4ba9a7f0ba2f", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "aws_internet_gateway", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:internet-gateway/igw-0a74c1fef454437c8", + "id": "igw-0a74c1fef454437c8", + "owner_id": "177559564758", + "region": "us-east-2", + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_route_table", + "name": "route_table", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:route-table/rtb-0f24beedbed7e02a3", + "id": "rtb-0f24beedbed7e02a3", + "owner_id": "177559564758", + "propagating_vgws": [], + "region": "us-east-2", + "route": [ + { + "carrier_gateway_id": "", + "cidr_block": "0.0.0.0/0", + "core_network_arn": "", + "destination_prefix_list_id": "", + "egress_only_gateway_id": "", + "gateway_id": "igw-0a74c1fef454437c8", + "ipv6_cidr_block": "", + "local_gateway_id": "", + "nat_gateway_id": "", + "network_interface_id": "", + "transit_gateway_id": "", + "vpc_endpoint_id": "", + "vpc_peering_connection_id": "" + } + ], + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "rtb-0f24beedbed7e02a3", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_internet_gateway.default", + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0c03eb59bd6270fcf", + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "All outbound traffic", + "from_port": 0, + "ipv6_cidr_blocks": [ + "::/0" + ], + "prefix_list_ids": [], + "protocol": "-1", + "security_groups": [], + "self": false, + "to_port": 0 + } + ], + "id": "sg-0c03eb59bd6270fcf", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "Docker webhook", + "from_port": 4444, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 4444 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "SSH from anywhere", + "from_port": 22, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 22 + } + ], + "name": "finus-default", + "name_prefix": "", + "owner_id": "177559564758", + "region": "us-east-2", + "revoke_rules_on_delete": false, + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "sg-0c03eb59bd6270fcf", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "services", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0e428a46b850e036a", + "description": "Managed by Terraform", + "egress": [], + "id": "sg-0e428a46b850e036a", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "TCP 3000 from anywhere", + "from_port": 3000, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 3000 + } + ], + "name": "finus-services", + "name_prefix": "", + "owner_id": "177559564758", + "region": "us-east-2", + "revoke_rules_on_delete": false, + "tags": null, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "sg-0e428a46b850e036a", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "status": "tainted", + "schema_version": 1, + "attributes": { + "arn": null, + "description": "Managed by Terraform", + "egress": [], + "id": "sg-09cfeb172160ecaa3", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "HTTP from anywhere", + "from_port": 80, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "http", + "security_groups": [], + "self": false, + "to_port": 80 + } + ], + "name": "finus-webserver", + "name_prefix": null, + "owner_id": null, + "region": "us-east-2", + "revoke_rules_on_delete": false, + "tags": null, + "tags_all": null, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_subnet", + "name": "dev", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-09d5718b3434ec2be", + "assign_ipv6_address_on_creation": false, + "availability_zone": "us-east-2c", + "availability_zone_id": "use2-az3", + "cidr_block": "10.0.128.0/17", + "customer_owned_ipv4_pool": "", + "enable_dns64": false, + "enable_lni_at_device_index": 0, + "enable_resource_name_dns_a_record_on_launch": false, + "enable_resource_name_dns_aaaa_record_on_launch": false, + "id": "subnet-09d5718b3434ec2be", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_cidr_block": "", + "ipv6_cidr_block_association_id": "", + "ipv6_ipam_pool_id": null, + "ipv6_native": false, + "ipv6_netmask_length": null, + "map_customer_owned_ip_on_launch": false, + "map_public_ip_on_launch": false, + "outpost_arn": "", + "owner_id": "177559564758", + "private_dns_hostname_type_on_launch": "ip-name", + "region": "us-east-2", + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "subnet-09d5718b3434ec2be", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_subnet", + "name": "prod", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-05e5676ab26e11acc", + "assign_ipv6_address_on_creation": false, + "availability_zone": "us-east-2c", + "availability_zone_id": "use2-az3", + "cidr_block": "10.0.0.0/17", + "customer_owned_ipv4_pool": "", + "enable_dns64": false, + "enable_lni_at_device_index": 0, + "enable_resource_name_dns_a_record_on_launch": false, + "enable_resource_name_dns_aaaa_record_on_launch": false, + "id": "subnet-05e5676ab26e11acc", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_cidr_block": "", + "ipv6_cidr_block_association_id": "", + "ipv6_ipam_pool_id": null, + "ipv6_native": false, + "ipv6_netmask_length": null, + "map_customer_owned_ip_on_launch": false, + "map_public_ip_on_launch": false, + "outpost_arn": "", + "owner_id": "177559564758", + "private_dns_hostname_type_on_launch": "ip-name", + "region": "us-east-2", + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "subnet-05e5676ab26e11acc", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", + "dependencies": [ + "aws_vpc.finus" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_vpc", + "name": "finus", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:vpc/vpc-0e13d5c774ed2b3c8", + "assign_generated_ipv6_cidr_block": false, + "cidr_block": "10.0.0.0/16", + "default_network_acl_id": "acl-0f7a72d6813ae088c", + "default_route_table_id": "rtb-09da1959a912933f3", + "default_security_group_id": "sg-06987cbe4f87d0594", + "dhcp_options_id": "dopt-01d5dfa449859477f", + "enable_dns_hostnames": false, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "id": "vpc-0e13d5c774ed2b3c8", + "instance_tenancy": "default", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "ipv6_cidr_block_network_border_group": "", + "ipv6_ipam_pool_id": "", + "ipv6_netmask_length": 0, + "main_route_table_id": "rtb-09da1959a912933f3", + "owner_id": "177559564758", + "region": "us-east-2", + "tags": { + "Name": "Finus" + }, + "tags_all": { + "Name": "Finus" + } + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "vpc-0e13d5c774ed2b3c8", + "region": "us-east-2" + }, + "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==" + } + ] + }, + { + "module": "module.dev", + "mode": "data", + "type": "aws_ami", + "name": "amazon_linux", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-2::image/ami-0b0b78dcacbab728f", + "block_device_mappings": [ + { + "device_name": "/dev/xvda", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "3000", + "snapshot_id": "snap-0afc5230d5113aa86", + "throughput": "125", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp3" + }, + "no_device": "", + "virtual_name": "" + } + ], + "boot_mode": "uefi-preferred", + "creation_date": "2026-03-03T18:01:43.000Z", + "deprecation_time": "2026-06-01T23:47:00.000Z", + "description": "Amazon Linux 2023 AMI 2023.10.20260302.1 x86_64 HVM kernel-6.1", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "image-id", + "values": [ + "ami-0b0b78dcacbab728f" + ] + } + ], + "hypervisor": "xen", + "id": "ami-0b0b78dcacbab728f", + "image_id": "ami-0b0b78dcacbab728f", + "image_location": "amazon/al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "v2.0", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": false, + "name": "al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "name_regex": null, + "owner_id": "137112412989", + "owners": null, + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-2", + "root_device_name": "/dev/xvda", + "root_device_type": "ebs", + "root_snapshot_id": "snap-0afc5230d5113aa86", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_eip", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-04f46a5b83e1a6af0", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-04f46a5b83e1a6af0", + "associate_with_private_ip": null, + "association_id": "eipassoc-0818aa83d85c2f91b", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-04f46a5b83e1a6af0", + "instance": "i-008147727278f7080", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-0f364cdcae030e0e6", + "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", + "private_ip": "10.0.221.90", + "ptr_record": "", + "public_dns": "ec2-16-58-118-74.us-east-2.compute.amazonaws.com", + "public_ip": "16.58.118.74", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-dev-backend" + }, + "tags_all": { + "Name": "finus-dev-backend" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.aws_instance.backend", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_eip", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-0857b784b20a92dad", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0857b784b20a92dad", + "associate_with_private_ip": null, + "association_id": "eipassoc-06e143e0fc9004868", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-0857b784b20a92dad", + "instance": "i-020ba441d6c3c0258", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-03283cda15cf431d3", + "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", + "private_ip": "10.0.140.152", + "ptr_record": "", + "public_dns": "ec2-3-142-125-202.us-east-2.compute.amazonaws.com", + "public_ip": "3.142.125.202", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-dev-webserver" + }, + "tags_all": { + "Name": "finus-dev-webserver" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.aws_instance.webserver", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_instance", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-008147727278f7080", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-008147727278f7080", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0f364cdcae030e0e6" + } + ], + "primary_network_interface_id": "eni-0f364cdcae030e0e6", + "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.221.90", + "public_dns": "", + "public_ip": "16.58.118.74", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-0c0b46050d4eb986e", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-09d5718b3434ec2be", + "tags": { + "Name": "finus-dev-backend" + }, + "tags_all": { + "Name": "finus-dev-backend" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0c03eb59bd6270fcf", + "sg-0e428a46b850e036a" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-008147727278f7080", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_instance", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-020ba441d6c3c0258", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-020ba441d6c3c0258", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-03283cda15cf431d3" + } + ], + "primary_network_interface_id": "eni-03283cda15cf431d3", + "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.140.152", + "public_dns": "", + "public_ip": "3.142.125.202", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-09e9f25279b1c2ac1", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-09d5718b3434ec2be", + "tags": { + "Name": "finus-dev-webserver" + }, + "tags_all": { + "Name": "finus-dev-webserver" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0c03eb59bd6270fcf" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-020ba441d6c3c0258", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.dev", + "aws_vpc.finus", + "module.dev.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.dev", + "mode": "managed", + "type": "aws_route_table_association", + "name": "assoc", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "gateway_id": "", + "id": "rtbassoc-04712335fcc923e95", + "region": "us-east-2", + "route_table_id": "rtb-0f24beedbed7e02a3", + "subnet_id": "subnet-09d5718b3434ec2be", + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_internet_gateway.default", + "aws_route_table.route_table", + "aws_subnet.dev", + "aws_vpc.finus" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "data", + "type": "aws_ami", + "name": "amazon_linux", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-2::image/ami-0b0b78dcacbab728f", + "block_device_mappings": [ + { + "device_name": "/dev/xvda", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "3000", + "snapshot_id": "snap-0afc5230d5113aa86", + "throughput": "125", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp3" + }, + "no_device": "", + "virtual_name": "" + } + ], + "boot_mode": "uefi-preferred", + "creation_date": "2026-03-03T18:01:43.000Z", + "deprecation_time": "2026-06-01T23:47:00.000Z", + "description": "Amazon Linux 2023 AMI 2023.10.20260302.1 x86_64 HVM kernel-6.1", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "image-id", + "values": [ + "ami-0b0b78dcacbab728f" + ] + } + ], + "hypervisor": "xen", + "id": "ami-0b0b78dcacbab728f", + "image_id": "ami-0b0b78dcacbab728f", + "image_location": "amazon/al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "v2.0", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": false, + "name": "al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", + "name_regex": null, + "owner_id": "137112412989", + "owners": null, + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-2", + "root_device_name": "/dev/xvda", + "root_device_type": "ebs", + "root_snapshot_id": "snap-0afc5230d5113aa86", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_eip", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-0262f07d6e23594c4", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0262f07d6e23594c4", + "associate_with_private_ip": null, + "association_id": "eipassoc-0202a50414bdbee94", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-0262f07d6e23594c4", + "instance": "i-0dd805bcaa0a0ade3", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-0d9461ec08710f2d7", + "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", + "private_ip": "10.0.50.231", + "ptr_record": "", + "public_dns": "ec2-3-150-23-72.us-east-2.compute.amazonaws.com", + "public_ip": "3.150.23.72", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-prod-backend" + }, + "tags_all": { + "Name": "finus-prod-backend" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.aws_instance.backend", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_eip", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "address": null, + "allocation_id": "eipalloc-06fe359af4fbd5f9b", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-06fe359af4fbd5f9b", + "associate_with_private_ip": null, + "association_id": "eipassoc-0ce3ab9543feb5d7f", + "carrier_ip": "", + "customer_owned_ip": "", + "customer_owned_ipv4_pool": "", + "domain": "vpc", + "id": "eipalloc-06fe359af4fbd5f9b", + "instance": "i-0f897d2768cad0033", + "ipam_pool_id": null, + "network_border_group": "us-east-2", + "network_interface": "eni-0e2de615da26dd0d9", + "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", + "private_ip": "10.0.57.91", + "ptr_record": "", + "public_dns": "ec2-18-190-215-135.us-east-2.compute.amazonaws.com", + "public_ip": "18.190.215.135", + "public_ipv4_pool": "amazon", + "region": "us-east-2", + "tags": { + "Name": "finus-prod-webserver" + }, + "tags_all": { + "Name": "finus-prod-webserver" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.aws_instance.webserver", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_instance", + "name": "backend", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0dd805bcaa0a0ade3", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0dd805bcaa0a0ade3", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0d9461ec08710f2d7" + } + ], + "primary_network_interface_id": "eni-0d9461ec08710f2d7", + "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.50.231", + "public_dns": "", + "public_ip": "3.150.23.72", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-0115ad31723e63439", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-05e5676ab26e11acc", + "tags": { + "Name": "finus-prod-backend" + }, + "tags_all": { + "Name": "finus-prod-backend" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0c03eb59bd6270fcf", + "sg-0e428a46b850e036a" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-0dd805bcaa0a0ade3", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_instance", + "name": "webserver", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-0b0b78dcacbab728f", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0f897d2768cad0033", + "associate_public_ip_address": true, + "availability_zone": "us-east-2c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0f897d2768cad0033", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.nano", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "finus-dev", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 2, + "http_tokens": "required", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0e2de615da26dd0d9" + } + ], + "primary_network_interface_id": "eni-0e2de615da26dd0d9", + "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.57.91", + "public_dns": "", + "public_ip": "18.190.215.135", + "region": "us-east-2", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 3000, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 125, + "volume_id": "vol-05d61c7cb18058c36", + "volume_size": 8, + "volume_type": "gp3" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-05e5676ab26e11acc", + "tags": { + "Name": "finus-prod-webserver" + }, + "tags_all": { + "Name": "finus-prod-webserver" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0c03eb59bd6270fcf" + ] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "i-0f897d2768cad0033", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.webserver", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_route_table_association", + "name": "assoc", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "gateway_id": "", + "id": "rtbassoc-0b1e2801d2830d94c", + "region": "us-east-2", + "route_table_id": "rtb-0f24beedbed7e02a3", + "subnet_id": "subnet-05e5676ab26e11acc", + "timeouts": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_internet_gateway.default", + "aws_route_table.route_table", + "aws_subnet.prod", + "aws_vpc.finus" + ] + } + ] + } + ], + "check_results": null +} diff --git a/deployment/main.tf b/deployment/main.tf deleted file mode 100644 index 88f8ce6..0000000 --- a/deployment/main.tf +++ /dev/null @@ -1,79 +0,0 @@ -provider "aws" { - region = "us-east-2" -} - -resource "aws_vpc" "finus" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = "Finus" - } -} - -resource "aws_subnet" "prod" { - vpc_id = aws_vpc.finus.id - cidr_block = "10.0.0.0/17" -} - -resource "aws_subnet" "dev" { - vpc_id = aws_vpc.finus.id - cidr_block = "10.0.128.0/17" -} - -resource "aws_internet_gateway" "default" { - vpc_id = aws_vpc.finus.id -} - -resource "aws_security_group" "default" { - name = "finus-default" - vpc_id = aws_vpc.finus.id - - ingress { - description = "SSH from anywhere" - cidr_blocks = ["0.0.0.0/0"] - from_port = "22" - to_port = "22" - protocol = "tcp" - } - - egress { - description = "HTTP from anywhere" - from_port = "3000" - to_port = "3000" - protocol = "tcp" - } -} - -resource "aws_route_table" "route_table" { - vpc_id = aws_vpc.finus.id - - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.default.id - } -} - -module "prod" { - source = "./modules/finus" - - environment_name = "prod" - ssh_key_pair_name = "finus-dev" - security_group_ids = [aws_security_group.default.id] - subnet_id = aws_subnet.prod.id - vpc_id = aws_vpc.finus.id - gateway_id = aws_internet_gateway.default.id - route_table_id = aws_route_table.route_table.id -} - - -module "dev" { - source = "./modules/finus" - - environment_name = "dev" - ssh_key_pair_name = "finus-dev" - security_group_ids = [aws_security_group.default.id] - subnet_id = aws_subnet.dev.id - vpc_id = aws_vpc.finus.id - gateway_id = aws_internet_gateway.default.id - route_table_id = aws_route_table.route_table.id -} diff --git a/deployment/scripts/install-deps.sh b/deployment/scripts/install-deps.sh new file mode 100755 index 0000000..c932239 --- /dev/null +++ b/deployment/scripts/install-deps.sh @@ -0,0 +1,30 @@ +#!/bin/bash +install() { + test -f "$(which $1 2>&1)" + if [[ ! $? -eq 0 ]]; then + echo "Installing $1" + sudo yum install $2 -y + else + echo "$1 already installed" + fi +} + +install docker docker +install node nodejs +install tmux tmux + +docker compose > /dev/null +if [[ ! $? -eq 0 ]]; then + echo "Installing docker compose" + #from https://gist.github.com/thimslugga/36019e15b2a47a48c495b661d18faa6d + sudo mkdir -p /usr/local/lib/docker/cli-plugins + sudo curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-"$(uname -m)" \ + -o /usr/local/lib/docker/cli-plugins/docker-compose + # Set ownership to root and make executable + test -f /usr/local/lib/docker/cli-plugins/docker-compose \ + && sudo chown root:root /usr/local/lib/docker/cli-plugins/docker-compose + test -f /usr/local/lib/docker/cli-plugins/docker-compose \ + && sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose +else + echo "Docker compose already installed" +fi diff --git a/deployment/scripts/start.sh b/deployment/scripts/start.sh new file mode 100755 index 0000000..5d3b009 --- /dev/null +++ b/deployment/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +sudo systemctl start docker +./upgrade-images.sh +tmux new-session -d -s upgrade-images 'node upgrade-on-new-image.js' diff --git a/deployment/scripts/upgrade-images.sh b/deployment/scripts/upgrade-images.sh new file mode 100755 index 0000000..a96c7ab --- /dev/null +++ b/deployment/scripts/upgrade-images.sh @@ -0,0 +1,3 @@ +#!/bin/bash +sudo docker compose down +sudo docker compose up -d --pull always diff --git a/deployment/scripts/upgrade-on-new-image.js b/deployment/scripts/upgrade-on-new-image.js new file mode 100644 index 0000000..a501332 --- /dev/null +++ b/deployment/scripts/upgrade-on-new-image.js @@ -0,0 +1,60 @@ +//Note: this code is all chatGPT written. + +const http = require('http'); +const { spawn } = require('child_process'); + +const PORT = 4444; +const SECOND = 1000; +const COOLDOWN = 10 * SECOND; + +let lastRun = 0; +let restarting = false; + +const server = http.createServer((req, res) => { + let body = ''; + + req.on('data', chunk => { + body += chunk; + }); + + req.on('end', () => { + const now = Date.now(); + + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + + if (now - lastRun < COOLDOWN) { + console.log(`[${new Date().toISOString()}] Webhook ignored due to cooldown`); + return + } + lastRun = now; + + if (restarting) { + console.log(`[${new Date().toISOString()}] Webhook ignored due to in-progress restart`); + return + } + restarting = true; + + console.log(`[${new Date().toISOString()}] Upgrading services...`); + const script = spawn('./upgrade-images.sh'); + + script.on('close', (code) => { + restarting = false; + if(code === 0) { + console.log(`[${new Date().toISOString()}] Finished upgrading services`); + } else { + console.log(`[${new Date().toISOString()}] Failed to upgrade services. Code: ${code}`); + } + }); + + script.on('error', (err) => { + console.error(`[${new Date().toISOString()}] Script error:`, err); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error'); + }); + }); +}); + +server.listen(PORT, () => { + console.log(`Webhook listener running on port ${PORT}`); +}); diff --git a/deployment/services/.env b/deployment/services/.env new file mode 100644 index 0000000..c3ae092 --- /dev/null +++ b/deployment/services/.env @@ -0,0 +1,3 @@ +#THIS ENV FILE IS NOT GIT IGNORED +DOCKER_HUB_USERNAME=measureonecodetwice +RELEASE_VERSION=latest diff --git a/deployment/services/default_container.env b/deployment/services/default_container.env new file mode 100644 index 0000000..ce8659e --- /dev/null +++ b/deployment/services/default_container.env @@ -0,0 +1,9 @@ +API_GATEWAY_ADDR=http://api-gateway:8000 +AUTH_SERVICE_ADDR=http://auth:8000 +USER_SERVICE_ADDR=http://user:8000 +ANALYTICS_SERVICE_ADDR=http://analytics:8000 +MARKET_SERVICE_ADDR=http://market:8000 + +MYSQL_HOST=finus-dev-database-1.cluster-cv8mki0o4cg9.us-east-2.rds.amazonaws.com +MYSQL_PORT=3306 +DB_NAME=finus diff --git a/deployment/services/docker-compose.yml b/deployment/services/docker-compose.yml new file mode 100644 index 0000000..bb14be4 --- /dev/null +++ b/deployment/services/docker-compose.yml @@ -0,0 +1,36 @@ +services: + api-gateway: + image: $DOCKER_HUB_USERNAME/finus-api-gateway:$RELEASE_VERSION + env_file: + - path: ./default_container.env + ports: + - "3000:8000" + depends_on: + - auth + - user + - analytics + - market + + auth: + image: $DOCKER_HUB_USERNAME/finus-auth:$RELEASE_VERSION + env_file: + - path: ./default_container.env + - path: ./secrets.env + + + user: + image: $DOCKER_HUB_USERNAME/finus-user:$RELEASE_VERSION + env_file: + - path: ./default_container.env + - path: ./secrets.env + + analytics: + image: $DOCKER_HUB_USERNAME/finus-analytics:$RELEASE_VERSION + env_file: + - path: ./default_container.env + - path: ./secrets.env + + market: + image: $DOCKER_HUB_USERNAME/finus-market:$RELEASE_VERSION + env_file: + - path: ./default_container.env diff --git a/deployment/services/secrets.env b/deployment/services/secrets.env new file mode 100644 index 0000000..f083420 --- /dev/null +++ b/deployment/services/secrets.env @@ -0,0 +1,5 @@ +#THIS ENV FILE IS NOT GIT IGNORED. +#REPLACE IT WITH THE REAL SECRETS FILE IN PROD +MYSQL_USER=dummyuser +MYSQL_PASSWORD=dummypassword +JWT_SECRET=dummysecret diff --git a/deployment/webserver/.env b/deployment/webserver/.env new file mode 100644 index 0000000..c3ae092 --- /dev/null +++ b/deployment/webserver/.env @@ -0,0 +1,3 @@ +#THIS ENV FILE IS NOT GIT IGNORED +DOCKER_HUB_USERNAME=measureonecodetwice +RELEASE_VERSION=latest diff --git a/deployment/webserver/docker-compose.yml b/deployment/webserver/docker-compose.yml new file mode 100644 index 0000000..353d002 --- /dev/null +++ b/deployment/webserver/docker-compose.yml @@ -0,0 +1,5 @@ +services: + webserver: + image: $DOCKER_HUB_USERNAME/finus-webserver:$RELEASE_VERSION + ports: + - "80:80" From 698f04f90c2e541efe63e95b90cd27c9a50d2b0a Mon Sep 17 00:00:00 2001 From: TebelR Date: Thu, 2 Apr 2026 14:20:53 -0500 Subject: [PATCH 193/204] Data input polish (#97) * Upgraded the UI for transactions table - combined the features from user data input and visualization into one * minor fix to account selection logic for creating transactions * Transactions table FOC * Reformatted accounts * Reformatted the projection page - there's a massive issue with savings projection at this point * addint unit tests for user input (WIP) * User input bug fixes for tests * FIxed massive savings projections and readded tests for new logic * Added import CSV button and removed old files * Rewrote unit tests to support new logic * Fixes the balance input fields * Turned the sub type input while editing to a selector * Updated coding style and cleaned up some directories * Bug fixes and cleanup for PR * Bug fixes with savings endpoint * accounts integration fix * Added a missing function mock in unit tests for transaction logic * Added load testing to handle 20 users and 200 requests per minute * Load tests fixed and should pass on the CI pipeline --------- Co-authored-by: Dylan Prabagaran Co-authored-by: Jackie Mei Co-authored-by: Deep Patel --- app/client/Dockerfile.base | 2 +- app/client/src/App.tsx | 3 +- app/client/src/api/Account.ts | 130 +-- app/client/src/api/DashboardAPI.ts | 186 ++++- app/client/src/api/Debt.ts | 46 +- app/client/src/api/GoalsAPI.ts | 6 - app/client/src/api/Saving.ts | 50 +- app/client/src/api/Transaction.ts | 135 +--- app/client/src/assets/react.svg | 1 - app/client/src/components/AccountForm.tsx | 283 ------- app/client/src/components/AccountList.tsx | 583 ++++++++++++-- app/client/src/components/AccountListCard.tsx | 74 -- app/client/src/components/CSVImportModal.tsx | 73 ++ app/client/src/components/SankeyChart.tsx | 9 +- app/client/src/components/TransactionCard.tsx | 70 -- app/client/src/components/TransactionList.tsx | 125 --- .../src/components/TransactionTable.tsx | 754 ++++++++++++++++-- app/client/src/components/TransationForm.tsx | 324 -------- .../src/components/csvread/CsvConfirm.tsx | 2 +- app/client/src/components/csvread/CsvDrop.tsx | 5 +- .../src/components/csvread/CsvPreview.tsx | 2 +- .../src/components/csvread/CsvUpload.tsx | 12 +- app/client/src/enum/TransactionCategory.ts | 2 +- app/client/src/index.css | 2 +- app/client/src/pages/DashboardPage.tsx | 8 +- app/client/src/pages/ProjectionPage.tsx | 553 ++++++------- app/client/src/types/AccountType.ts | 7 +- app/client/src/types/Transaction.ts | 3 +- app/client/src/utils/handleInput.ts | 40 +- app/server/docker-compose.test.yml | 14 + app/server/load-tests/README.md | 25 + app/server/load-tests/api-gateway-capacity.js | 221 +++++ .../python/analytics/src/logic/savings.py | 156 +--- .../python/analytics/test/test_savings.py | 210 ++++- .../services/ts/api-gateway/src/index.ts | 2 + .../ts/user/Dockerfile.integration.test | 2 + .../services/ts/user/Dockerfile.userInp.test | 11 + .../ts/user/integration/accounts.test.ts | 5 +- app/server/services/ts/user/src/CheckUser.ts | 78 +- app/server/services/ts/user/src/index.ts | 232 +++++- .../services/ts/user/src/logic/goals.ts | 16 +- .../ts/user/src/logic/transactions.ts | 48 +- .../ts/user/src/queries/transactions.ts | 188 ++++- .../services/ts/user/src/routes/account.ts | 15 +- .../ts/user/src/routes/transaction.ts | 4 + app/server/services/ts/user/stryker.conf.json | 9 - .../services/ts/user/strykerNODE.conf.json | 12 - .../ts/user/test/accountTests.test.ts | 163 ++++ .../ts/user/test/logic_transactions.test.ts | 15 +- .../services/ts/user/test/transaction.test.ts | 201 +++++ .../services/ts/user/vitest.config.stryker.ts | 13 - .../Goals/GoalsAcceptance.md} | 0 documentation/Coding_Style.md | 58 ++ documentation/Finus_Test_Plan.md | 16 +- 54 files changed, 3389 insertions(+), 1815 deletions(-) delete mode 100644 app/client/src/assets/react.svg delete mode 100644 app/client/src/components/AccountForm.tsx delete mode 100644 app/client/src/components/AccountListCard.tsx create mode 100644 app/client/src/components/CSVImportModal.tsx delete mode 100644 app/client/src/components/TransactionCard.tsx delete mode 100644 app/client/src/components/TransactionList.tsx delete mode 100644 app/client/src/components/TransationForm.tsx create mode 100644 app/server/load-tests/README.md create mode 100644 app/server/load-tests/api-gateway-capacity.js create mode 100644 app/server/services/ts/user/Dockerfile.userInp.test delete mode 100644 app/server/services/ts/user/stryker.conf.json delete mode 100644 app/server/services/ts/user/strykerNODE.conf.json create mode 100644 app/server/services/ts/user/test/accountTests.test.ts create mode 100644 app/server/services/ts/user/test/transaction.test.ts delete mode 100644 app/server/services/ts/user/vitest.config.stryker.ts rename documentation/{acceptance tests/goals.md => Acceptance/Goals/GoalsAcceptance.md} (100%) create mode 100644 documentation/Coding_Style.md diff --git a/app/client/Dockerfile.base b/app/client/Dockerfile.base index a5d1461..92b7333 100644 --- a/app/client/Dockerfile.base +++ b/app/client/Dockerfile.base @@ -2,7 +2,7 @@ FROM oven/bun:1.3.9-slim WORKDIR /app -COPY package.json bun.lockb . +COPY package.json bun.lockb ./ RUN --mount=type=cache,target=/root/.bun/install/cache \ bun install diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index 9892c57..c13df41 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -199,7 +199,6 @@ function App() { } /> - {session && ( }> )} - + } /> {/**Code below is only used for dashboard development purposes */} diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts index df93723..417984c 100644 --- a/app/client/src/api/Account.ts +++ b/app/client/src/api/Account.ts @@ -1,129 +1,83 @@ import type { updateResponse } from "../types/responseTypes"; import type { Account } from "../types/AccountType"; -import type { AuthSession } from "@/types/authTypes"; -import { BASE_URL } from "@/utils/constants"; -const requestUrl = `${BASE_URL}/api/accounts`; +import { instance } from "./config"; +// import { data } from "react-router-dom"; //Sends a request to get different accounts the user has -export async function getUserAccounts( - session: AuthSession, - type?: string, -): Promise { - let url = requestUrl; - - //Determine if we're targetting a specific type - if (type) { - url = `${requestUrl}/?type=${type}`; - } - +export async function getUserAccounts(type?: string): Promise { try { - //Sends a http request and waits for a response - const response = await fetch(url, { - method: "GET", - headers: { Authorization: `Bearer ${session.token}` }, - }); + const response = await instance.get( + `/api/accounts${type ? `?type=${type}` : ""}`, + ); - //Determine if we were able to retrieve user's data - if (!response.ok) { - //Failed to retrieve user data, return empty array - console.error( - "Error: Failed to retrieve users accounts ", - response.status, - ); - return []; + //determine if we were able to retrieve user's data + if (response.status === 200 || response.status === 201) { + // console.log("Account created:", response.data); + return response.data; + } else { + console.error("Failed to create account", response.status); + throw new Error(`Failed to create account: ${response.statusText}`); } - - return response.json(); } catch (error) { - console.error(error); + console.error("Error creating account:", error); throw error; } } //Sends a post request to create a user account export async function postUserAccount( - session: AuthSession, newAccount: Account, ): Promise { try { - //Create post request and wait for response - const response = await fetch(requestUrl, { - method: "POST", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify(newAccount), - }); - - //Determine if our post was a success - if (response.ok) { - alert("Account " + newAccount.name + " has been created"); + const response = await instance.post(`/api/accounts`, newAccount); + if (response.status === 200 || response.status === 201) { + console.log("Account created:", response.data); + return newAccount; } else { - alert("Failed to create account " + newAccount.name); - console.error(response.status); + console.error("Failed to create account", response.status); + throw new Error(`Failed to create account: ${response.statusText}`); } - - return response.json(); } catch (error) { - console.error(error); + console.error("Error creating account:", error); throw error; } } //Updates the users account export async function putUserAccount( - session: AuthSession, newAccount: Account, ): Promise { try { - //Create post request and wait for response - const response = await fetch(requestUrl, { - method: "PUT", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify(newAccount), - }); + const response = await instance.put(`/api/accounts`, newAccount); - //Determine if our post was a success - if (response.ok) { - alert("Account " + newAccount.name + " has been updated"); + if (response.status === 200) { + console.log("Account updated:", response.data); + return response.data; } else { - alert("Failed to update account " + newAccount.name); - console.error(response.status); + console.error("Failed to update account", response.status); + throw new Error(`Failed to update account: ${response.statusText}`); } - - return response.json(); } catch (error) { - console.error(error); + console.error("Error updating account:", error); throw error; } } -export async function deleteUserAccount( - session: AuthSession, - userAccount: Account, -) { - const content = JSON.stringify({ id: userAccount.id }); - //Create delete request to delete the account - const response = await fetch(requestUrl, { - method: "DELETE", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: content, - }); +export async function deleteUserAccount(accountId: number): Promise { + try { + const id = Number(accountId); + const response = await instance.delete(`/api/accounts?id=${id}`); - if (response.ok) { - alert("Account " + userAccount.name + " has been deleted"); - } else { - alert("Failed to delete account " + userAccount.name); - console.error(response.status); + if (response.status === 200) { + console.log("Account deleted successfully"); + return true; + } else { + console.error("Failed to delete account", response.status); + return false; + } + } catch (error) { + console.error("Error deleting account:", error); + return false; } - - return response.ok; } diff --git a/app/client/src/api/DashboardAPI.ts b/app/client/src/api/DashboardAPI.ts index 076d83e..b17285c 100644 --- a/app/client/src/api/DashboardAPI.ts +++ b/app/client/src/api/DashboardAPI.ts @@ -4,6 +4,7 @@ import type { Transaction } from "@/types/Transaction"; import { instance } from "./config"; import type { BudgetWithExpenditure } from "@/types/BudgetWithExpenditure"; import type { SnapshotData } from "@/types/AggregatedSnapshot"; +import type { Account, MinimizedAccount } from "@/types/AccountType"; async function getTransactions(): Promise { try { @@ -19,15 +20,21 @@ async function getTransactions(): Promise { } const output: Transaction[] = []; for (let i = 0; i < response.data.length; i++) { + let formattedDate = response.data[i]["date"]; + if (formattedDate) { + formattedDate = formattedDate.split("T")[0]; //database stores transacitons as datetime so split to get date and disregard time + } + output.push({ id: response.data[i]["id"], financialAccount_id: response.data[i]["financialAccount_id"], amount: response.data[i]["amount"], category: response.data[i]["category"], - date: response.data[i]["date"], + date: formattedDate, sender: response.data[i]["sender"], recipient: response.data[i]["recipient"], description: response.data[i]["description"], + account_name: response.data[i]["account_name"], }); } @@ -38,6 +45,98 @@ async function getTransactions(): Promise { } } +//deletes a single transaciton based on id - returns true if successful, false otherwise +export async function deleteTransaction( + transactionId: number, +): Promise { + try { + const response = await instance.delete( + `/table/transactions?tid=${transactionId}`, + ); + if (response.status !== 200) { + console.error("Failed to delete transaction", response.status); + return false; + } + return true; + } catch (error) { + console.error("Error deleting transaction:", error); + return false; + } +} + +//updates a single transaciton based on id - returns true if successful, false otherwise +export async function updateTransaction( + transaction: Transaction, +): Promise { + try { + const response = await instance.patch( + `/table/transactions?tid=${transaction.id}`, + transaction, + ); + + if (response.status !== 200) { + console.error("Failed to update transaction", response.status); + return false; + } + + return true; + } catch (error) { + console.error("Error updating transaction:", error); + return false; + } +} + +//creates a single transaciton - returns the created transaction with id if successful, throws error otherwise +export async function createTransaction( + transaction: Transaction, +): Promise { + try { + const response = await instance.post( + `/table/transactions?fid=${transaction.financialAccount_id}`, + transaction, + ); + if (response.status !== 200) { + throw new Error(`Failed to create transaction: ${response.statusText}`); + } + return response.data; + } catch (error) { + console.error("Error creating transaction:", error); + throw error; + } +} + +//gets a map of account ids to account names for a user id +export async function getAccountIdsForUser(): Promise< + MinimizedAccount[] | null +> { + try { + const response = await instance.get(`/table/transactions/accounts`); + + if (response.status !== 200) { + throw new Error( + `Failed to fetch account IDs for user: ${response.statusText}`, + ); + } + + if (!response.data || !Array.isArray(response.data)) { + return null; + } + + //directly map the array + const output: MinimizedAccount[] = response.data.map( + (account: MinimizedAccount) => ({ + id: account.id, + name: account.name, + }), + ); + + return output; + } catch (error) { + console.error("Error fetching account IDs for user:", error); + throw error; + } +} + export async function getSnapshotData(): Promise { try { const response = await instance.get("/table/snapshot"); @@ -138,7 +237,6 @@ async function getSavingsContribChartData( ); } const response = await instance.get(`/charts/savings?period=${period}`); - console.log("received savings data", response); if (response.status !== 200) { throw new Error( `Failed to fetch savings contribution chart data: ${response.statusText}`, @@ -192,6 +290,90 @@ async function getIncomeFlowChartData( } } +export async function getUserAccounts(): Promise { + try { + const response = await instance.get(`/api/accounts`); + + if (response.status !== 200) { + throw new Error(`Failed to fetch accounts: ${response.statusText}`); + } + if (!response.data) { + return null; + } + + const output: Account[] = []; + for (let i = 0; i < response.data.length; i++) { + output.push({ + id: response.data[i]["id"], + name: response.data[i]["name"], + type: response.data[i]["type"], + balance: response.data[i]["balance"], + value: response.data[i]["value"] || response.data[i]["balance"], + subtype: response.data[i]["subtype"], + last_updated: response.data[i]["last_updated"], + }); + } + + return output; + } catch (error) { + console.error("Error fetching accounts:", error); + throw error; + } +} + +//create a new account +export async function createAccount( + accountData: Omit, +): Promise { + try { + const response = await instance.post(`/api/accounts`, accountData); + + if (response.status !== 200) { + throw new Error(`Failed to create account: ${response.statusText}`); + } + + return response.data; + } catch (error) { + console.error("Error creating account:", error); + throw error; + } +} + +//update an existing account +export async function updateAccount( + accountData: Partial & { id: number }, +): Promise { + try { + const response = await instance.put( + `/api/accounts/${accountData.id}`, + accountData, + ); + + if (response.status !== 200) { + throw new Error(`Failed to update account: ${response.statusText}`); + } + + return response.data; + } catch (error) { + console.error("Error updating account:", error); + throw error; + } +} + +//delete an account +export async function deleteAccount(accountId: number): Promise { + try { + const response = await instance.delete(`/api/accounts/${accountId}`); + + if (response.status !== 200) { + throw new Error(`Failed to delete account: ${response.statusText}`); + } + } catch (error) { + console.error("Error deleting account:", error); + throw error; + } +} + export { getBudgetWithExpenditure, getTransactions, diff --git a/app/client/src/api/Debt.ts b/app/client/src/api/Debt.ts index 6b32969..afbf575 100644 --- a/app/client/src/api/Debt.ts +++ b/app/client/src/api/Debt.ts @@ -1,27 +1,29 @@ import type { DebtPayoffResponse } from "../types/responseTypes"; -import type { AuthSession } from "@/types/authTypes"; +// import type { AuthSession } from "@/types/authTypes"; import type { Account } from "@/types/AccountType"; import type { projectionDebtRequest } from "@/types/requestTypes"; +import { instance } from "./config"; -const requestUrl = "http://localhost:3000/api/debts"; +// const requestUrl = "http://localhost:3000/api/debts"; //Sends a request to get different debts the user has -export async function getDebt(session: AuthSession): Promise { +export async function getDebt(): Promise { try { //Sends a http request and waits for a response - const response = await fetch(requestUrl, { - method: "GET", - headers: { Authorization: `Bearer ${session.token}` }, - }); + // const response = await fetch(requestUrl, { + // method: "GET", + // headers: { Authorization: `Bearer ${session.token}` }, + // }); + const response = await instance.get(`/api/debts`); //Determine if we were able to retrieve user's data - if (!response.ok) { + if (response.status !== 200) { //Failed to retrieve user data, return empty array console.error("Error: Failed to retrieve users debts", response.status); return []; } - return response.json(); + return response.data; } catch (error) { console.error(error); throw error; @@ -30,28 +32,28 @@ export async function getDebt(session: AuthSession): Promise { //Post request, even tho it says get in the function export async function getDebtProjection( - session: AuthSession, + // session: AuthSession, request: projectionDebtRequest, ): Promise { - const url = "http://localhost:3000/predict-debt-payoff"; + // const url = "http://localhost:3000/predict-debt-payoff"; try { //Create post request and wait for response - const response = await fetch(url, { - method: "POST", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify(request), - }); - + // const response = await fetch(url, { + // method: "POST", + // headers: { + // "content-type": "application/json", + // Authorization: `Bearer ${session.token}`, + // }, + // body: JSON.stringify(request), + // }); + const response = await instance.post(`/predict-debt-payoff`, request); //Determine if our post was a success - if (!response.ok) { + if (response.status !== 200) { console.error(response.status); } - return response.json(); + return response.data; } catch (error) { console.error(error); throw error; diff --git a/app/client/src/api/GoalsAPI.ts b/app/client/src/api/GoalsAPI.ts index c9cbb9a..4a024d0 100644 --- a/app/client/src/api/GoalsAPI.ts +++ b/app/client/src/api/GoalsAPI.ts @@ -2,23 +2,19 @@ import type { Goal } from "../types/goals.ts"; import { instance } from "./config"; async function fetchGoals(): Promise { - console.log("fetching goals"); const response = await instance.get("/goals"); if (response.status !== 200) { throw new Error(`Failed to fetch goals data: ${response.statusText}`); } - console.log("returning a list of goals: ", response.data); return response.data; } async function createGoal(goal: Partial): Promise { - console.log("creating goal: ", goal); const response = await instance.post("/goals", goal); if (response.status !== 201) { // 201 Created is standard for POST throw new Error(`Failed to create goal: ${response.statusText}`); } - console.log("got response from create goal: ", response.data); return response.data; } @@ -31,12 +27,10 @@ async function updateGoal( throw new Error(`Failed to edit goal: ${response.statusText}`); } - // console.log("updated goal: ", response.data); return response.data; } async function deleteGoal(goalId: string): Promise { - // console.log("deleting goal: ", goalId); const response = await instance.delete(`/goals?gid=${goalId}`); if (response.status !== 204) { // 204 No Content is standard for DELETE diff --git a/app/client/src/api/Saving.ts b/app/client/src/api/Saving.ts index 6215858..45412a2 100644 --- a/app/client/src/api/Saving.ts +++ b/app/client/src/api/Saving.ts @@ -1,26 +1,28 @@ import { type Account } from "@/types/AccountType"; -import { type AuthSession } from "@/types/authTypes"; +// import { type AuthSession } from "@/types/authTypes"; import type { projectionSavingRequest } from "@/types/requestTypes"; import { type savingProjectionResponseData } from "@/types/responseTypes"; -const requestUrl = "http://localhost:3000/api/savings"; +// const requestUrl = "http://localhost:3000/api/savings"; +import { instance } from "./config"; //Sends a request to get different debts the user has -export async function getSaving(session: AuthSession): Promise { +export async function getSaving(): Promise { try { //Sends a http request and waits for a response - const response = await fetch(requestUrl, { - method: "GET", - headers: { Authorization: `Bearer ${session.token}` }, - }); + // const response = await fetch(requestUrl, { + // method: "GET", + // headers: { Authorization: `Bearer ${session.token}` }, + // }); + const response = await instance.get(`/api/savings`); //Determine if we were able to retrieve user's data - if (!response.ok) { + if (response.status !== 200) { //Failed to retrieve user data, return empty array console.error("Error: Failed to retrieve users debts", response.status); return []; } - return response.json(); + return response.data; } catch (error) { console.error(error); throw error; @@ -29,29 +31,35 @@ export async function getSaving(session: AuthSession): Promise { //Post request, even tho it says get in the function export async function getSavingProjection( - session: AuthSession, + // session: AuthSession, request: projectionSavingRequest, ): Promise { - const url = "http://localhost:3000/compound-interest"; + // const url = "http://localhost:3000/compound-interest"; try { //Create post request and wait for response - const response = await fetch(url, { - method: "POST", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify(request), - }); + // const response = await fetch(url, { + // method: "POST", + // headers: { + // "content-type": "application/json", + // Authorization: `Bearer ${session.token}`, + // }, + // body: JSON.stringify(request), + // }); + console.log("sent over: ", JSON.stringify(request)); + const response = await instance.post( + `/compound-interest`, + JSON.stringify(request), + ); + console.log("response from server: ", response); //Determine if our post was a success - if (!response.ok) { + if (response.status !== 200) { console.error(response.status); return []; } - return response.json(); + return response.data; } catch (error) { console.error(error); throw error; diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts index 2eed076..eff3752 100644 --- a/app/client/src/api/Transaction.ts +++ b/app/client/src/api/Transaction.ts @@ -1,125 +1,13 @@ -import type { AuthSession } from "@/types/authTypes"; -import type { updateResponse } from "../types/responseTypes"; +// import type { AuthSession } from "@/types/authTypes"; +// import type { updateResponse } from "../types/responseTypes"; import type { Transaction } from "../types/Transaction"; import type { TransactionDraft } from "@/utils/ConvertTransaction"; -import { BASE_URL } from "@/utils/constants"; +import { instance } from "./config"; -const requestUrl = `${BASE_URL}/api/transactions`; - -//Sends a GET request to get the list of user transactions for the account -export async function getTransactions( - session: AuthSession, - financialAccount_id: number, -): Promise { - try { - const response = await fetch( - `${requestUrl}/?financialAccount_id=${financialAccount_id}`, - { - method: "GET", - headers: { Authorization: `Bearer ${session.token}` }, - }, - ); - - if (!response.ok) { - alert("Failed to retrieve account's transaction\n"); - console.error(response.status); - return []; - } - - return response.json(); - } catch (error) { - console.error(error); - throw error; - } -} - -//Can send multiple transactions in a push request -export async function postTranscations( - session: AuthSession, - trans: Transaction, -): Promise { - try { - const response = await fetch(requestUrl, { - method: "POST", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify(trans), - }); - - if (response.ok) { - alert("Create the transaction"); - } else { - alert("Failed to create transaction"); - console.error(response.status); - } - - return response.json(); - } catch (error) { - console.log(error); - throw error; - } -} -// PUT /api/transactions/ -export async function putTranscations( - session: AuthSession, - trans: Transaction, -): Promise { - try { - const response = await fetch(requestUrl, { - method: "PUT", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify(trans), - }); - - if (!response.ok) { - console.error("Failed to update transaction", response.status); - } - - return response.ok; - } catch (error) { - console.error(error); - throw error; - } -} - -//DELETE /api/transactions/ - -export async function deleteTransaction( - session: AuthSession, - selectedTransaction: Transaction, -): Promise { - try { - const response = await fetch(requestUrl, { - method: "DELETE", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify({ - id: selectedTransaction.id, - financialAccount_id: selectedTransaction.financialAccount_id, - }), - }); - - if (!response.ok) { - console.error("Failed to delete transaction", response.status); - } - - return response.ok; - } catch (error) { - console.error(error); - throw error; - } -} // POST /api/transactions/csvTransaction export async function uploadCsvTransactions( - session: AuthSession, + // session: AuthSession, financialAccount_id: number, transactions: TransactionDraft[], ): Promise<{ @@ -127,14 +15,9 @@ export async function uploadCsvTransactions( skipped: number; transactions: Transaction[]; }> { - const response = await fetch(`${requestUrl}/csvTransaction`, { - method: "POST", - headers: { - "content-type": "application/json", - Authorization: `Bearer ${session.token}`, - }, - body: JSON.stringify({ financialAccount_id, transactions }), - }); - - return response.json(); + const response = await instance.post( + `/api/transactions/csvTransaction`, + JSON.stringify({ financialAccount_id, transactions }), + ); + return response.data; } diff --git a/app/client/src/assets/react.svg b/app/client/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/app/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx deleted file mode 100644 index 7ee8513..0000000 --- a/app/client/src/components/AccountForm.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import React, { useState } from "react"; -import "./userForm.css"; -import { postUserAccount, putUserAccount } from "../api/Account.ts"; -import { type Account } from "../types/AccountType.ts"; -import { validateAccountForm } from "../utils/ValidateForms.ts"; -import { - handleCurrencyChange, - handleCurrencyBlur, -} from "../utils/handleInput.ts"; -import { accountCategory } from "@/enum/AccountCategory.ts"; -import type { AuthSession } from "@/types/authTypes.ts"; - -interface popupProp { - toggle: () => void; - session: AuthSession; - setAccount?: (account: Account) => void; - addAccount?: (account: Account) => void; - edit: boolean; - selectedAccount?: Account; -} - -type typeofAccount = keyof typeof accountCategory; - -export default function PopupForm({ - toggle, - session, - setAccount, - addAccount, - edit, - selectedAccount, -}: popupProp) { - //Submits the account data - - const handleSubmit = () => { - let subtype = undefined; - let interest = undefined; - const accountBalance = Number(balance); - - if ( - (accountType === "SAVING" || accountType === "CREDIT_CARD") && - formInput.subType - ) { - subtype = formInput.subType; - } - - if (accountType === accountCategory.SAVING) { - interest = formInput.interest; - } - - //Check account before validating b/c account can be undefined - //Determine if form input for an account is valid - if ( - accountType && - validateAccountForm( - formInput.name, - accountCategory[accountType], - accountBalance, - undefined, - subtype, - interest, - ) - ) { - const newAccount: Account = { - id: 0, - name: formInput.name, - type: accountType, - balance: accountBalance, - subtype: subtype, - value: 0, - last_updated: new Date(), - }; - console.log(newAccount); - - //Determine if we editing an account info - if (edit && selectedAccount) { - newAccount.id = selectedAccount.id; - - try { - //Put request to update the account - putUserAccount(session, newAccount).then((response) => { - //Determine if sucessfully updated the account - if (response && response.lastUpdated && setAccount) { - newAccount.last_updated = response.lastUpdated; - newAccount.balance = Number(newAccount.balance.toFixed(2)); - setAccount(newAccount); - } - }); - } catch { - alert("Failed to update Account"); - } - } else { - try { - //Send a post request to create - postUserAccount(session, newAccount).then((response) => { - //When creating new account, id and lastupdated is returned for that account - if (response && response.id) { - newAccount.id = response.id; - } - - if (response.lastUpdated) { - newAccount.last_updated = response.lastUpdated; - - //Check if addAcount is undefined - if (addAccount) { - addAccount(newAccount); - } - } - }); - } catch { - alert("Failed to create account " + newAccount.name); - } - } - - toggle(); - } else { - alert("Please fill out the form"); - } - }; - - const handleChange = ( - event: React.ChangeEvent, - ) => { - setFormInput({ ...formInput, [event.target.name]: event.target.value }); - }; - - //Handles the bank statement - /*const handleFile = (event:React.ChangeEvent) => { - - if(event.target && event.target.files && event.target.files[0]){ - setFile(event.target.files[0]) - } else { - setFile(undefined) - } - }*/ - - const [formInput, setFormInput] = useState(() => { - if (edit && selectedAccount) { - return { - name: selectedAccount.name, - subType: selectedAccount.subtype || "", - interest: 0, - }; - } else { - //Default value - return { - name: "", - subType: "", - interest: 0, - }; - } - }); - //const [file, setFile] = useState(undefined) - const [balance, setBalance] = useState(() => { - if (edit && selectedAccount) { - return selectedAccount.balance.toString(); - } else { - //Default value - return ""; - } - }); - - const [accountType, setAccountType] = useState( - () => { - if (edit && selectedAccount) { - console.log(selectedAccount.type); - return selectedAccount.type as typeofAccount; - } else { - //Default value - return undefined; - } - }, - ); - - const accountCat: typeofAccount[] = Object.keys( - accountCategory, - ) as typeofAccount[]; - - return ( - <> -

-
- {edit ? ( -

Edit Account

- ) : ( -

Create Account

- )} - - - -

- - - -

- - - handleCurrencyChange(event, setBalance)} - onBlur={(event) => handleCurrencyBlur(event, balance, setBalance)} - placeholder="0.00" - /> -

- - {accountType === "SAVINGS" ? ( - <> - - -

- - ) : null} - - {accountType === "CREDIT_CARD" ? ( - <> - - - - ) : null} - -
- - {edit ? ( - - ) : ( - - )} -
-
-
- - ); -} diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx index 34d2530..2b8f3c1 100644 --- a/app/client/src/components/AccountList.tsx +++ b/app/client/src/components/AccountList.tsx @@ -1,86 +1,553 @@ -import { useEffect, useState } from "react"; -import { getUserAccounts } from "../api/Account"; +import { useEffect, useState, useMemo } from "react"; +import { + getUserAccounts, + postUserAccount, + putUserAccount, + deleteUserAccount, +} from "../api/Account"; import { type Account } from "../types/AccountType"; -import AccountListCard from "./AccountListCard"; -import AccountPopup from "./AccountForm"; +import { AiOutlinePlus, AiOutlineClose, AiOutlineSearch } from "react-icons/ai"; +import { FiEdit2 } from "react-icons/fi"; +import NoItemState from "./NoItemState"; import type { AuthSession } from "@/types/authTypes"; -import "./userForm.css"; -interface listProp { +import { + handleCurrencyBlur, + handleCurrencyChange, + handleListCurrencyChange, + handleListCurrencyBlur, +} from "@/utils/handleInput"; + +interface AccountListProps { session: AuthSession; } -export default function AccountList({ session }: listProp) { - //Stores a lits of the user's account - const [userAccounts, setUserAccounts] = useState([]); +interface EditableAccount extends Account { + isExpanded?: boolean; +} - const [seen, setSeen] = useState(false); +function AccountList({ session }: AccountListProps) { + const [accounts, setAccounts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [showAddForm, setShowAddForm] = useState(false); + const [editingValues, setEditingValues] = useState<{ + [key: number]: Partial; + }>({}); - //Try to get the accounts from the server + // Fetch accounts on mount useEffect(() => { - try { - getUserAccounts(session).then((accounts) => { - //Determine if we acquired the accounts - if (accounts) { - console.log(accounts); - setUserAccounts(accounts); - } - }); - } catch { - alert("Failed to retrieve user accounts"); - } + const fetchAccounts = async () => { + try { + setIsLoading(true); + const data = await getUserAccounts(); + setAccounts((data || []).map((acc) => ({ ...acc, isExpanded: false }))); + } catch (error) { + console.error("Error fetching accounts:", error); + } finally { + setIsLoading(false); + } + }; + fetchAccounts(); }, [session]); - //Adds a account to the list - const addAccount = (newAccount: Account) => { - setUserAccounts((userAccounts) => [...userAccounts, newAccount]); + // Filter accounts based on search + const filteredAccounts = useMemo(() => { + if (!searchTerm.trim()) return accounts; + const term = searchTerm.toLowerCase(); + return accounts.filter( + (acc) => + acc.name.toLowerCase().includes(term) || + acc.type.toLowerCase().includes(term) || + (acc.subtype && acc.subtype.toLowerCase().includes(term)), + ); + }, [accounts, searchTerm]); + + // format helpers + const formatCurrency = (amount: number | string): string => { + //convert to number if it's a string + const numAmount = typeof amount === "string" ? parseFloat(amount) : amount; + + //check if conversion was successful + if (isNaN(numAmount)) { + return "$0.00"; + } + + if (numAmount < 0) { + return `-$${Math.abs(numAmount).toFixed(2)}`; + } + return `$${numAmount.toFixed(2)}`; }; - //Removes any account from the list thats shares the same id - const removeAccount = (remove: Account) => { - setUserAccounts((userAccounts) => - userAccounts.filter((account) => account.id !== remove.id), - ); + const formatDate = (dateStr: string): string => { + if (!dateStr) return "N/A"; + return new Date(dateStr).toLocaleDateString(); }; - const setAccount = (editAccount: Account) => { - setUserAccounts( - userAccounts.map((account) => - account.id === editAccount.id ? { ...editAccount } : account, + // expand/collapse row + const toggleExpand = (accountId: number) => { + setAccounts((prev) => + prev.map((acc) => + acc.id === accountId ? { ...acc, isExpanded: !acc.isExpanded } : acc, ), ); + + const account = accounts.find((a) => a.id === accountId); + if (account && !editingValues[accountId]) { + setEditingValues((prev) => ({ + ...prev, + [accountId]: { + name: account.name, + type: account.type, + balance: account.balance, + subtype: account.subtype, + }, + })); + } + }; + + // handle field changes + const handleFieldChange = ( + accountId: number, + field: string, + value: string | number, + ) => { + setEditingValues((prev) => ({ + ...prev, + [accountId]: { + ...prev[accountId], + [field]: value, + }, + })); + }; + + // save edited account + const handleSaveEdit = async (accountId: number) => { + const updates = editingValues[accountId]; + if (!updates) return; + + try { + const updatedAccount = await putUserAccount({ + ...updates, + balance: Number(updates.balance), + id: accountId, + } as Account); + if (updatedAccount) { + setAccounts((prev) => + prev.map((acc) => + acc.id === accountId + ? { ...acc, ...updates, isExpanded: false } + : acc, + ), + ); + setEditingValues((prev) => { + const newState = { ...prev }; + delete newState[accountId]; + return newState; + }); + } + } catch (error) { + console.error("Failed to update account:", error); + alert("Failed to update account"); + } + }; + + // delete account + const handleDelete = async (accountId: number) => { + if ( + !confirm( + "Are you sure you want to delete this account? This will also delete all associated transactions.", + ) + ) + return; + + try { + const accountToDelete = accounts.find((a) => a.id === accountId); + if (accountToDelete) { + const success = await deleteUserAccount(accountToDelete.id); + if (success) { + setAccounts((prev) => prev.filter((acc) => acc.id !== accountId)); + } + } + } catch (error) { + console.error("Failed to delete account:", error); + alert("Failed to delete account"); + } }; - const toggle = () => { - setSeen(!seen); + // new account state + const [newAccount, setNewAccount] = useState({ + name: "", + type: "", + subtype: "", + }); + + const [newBalance, setNewBalance] = useState(""); + + const handleAddAccount = async () => { + if (!newAccount.name || !newAccount.type) { + alert("Please fill in required fields (name and type)"); + return; + } + + try { + const balance = Number(newBalance); + const created = await postUserAccount({ + id: 0, + name: newAccount.name, + type: newAccount.type, + subtype: newAccount.subtype || null, + balance: balance, + value: balance, + last_updated: new Date().toISOString(), + } as Account); + + setAccounts( + (prev) => + [{ ...created, isExpanded: false }, ...prev] as EditableAccount[], + ); + setNewAccount({ name: "", type: "", subtype: "" }); + setNewBalance(""); + setShowAddForm(false); + } catch (error) { + console.error("Failed to create account:", error); + alert("Failed to create account"); + } }; + if (isLoading) { + return ( +
+
Loading accounts...
+
+ ); + } + + if (accounts.length === 0 && !showAddForm) { + return ( +
+ 💰
} + /> +
+ +
+
+ ); + } + return ( - <> -
-
- {userAccounts.map((account) => ( - + {/* Header with search and add button */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-black/50 border border-green-500/20 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-green-500" + /> +
+ +
+ + {/* Add Account Form */} + {showAddForm && ( +
+
+

New Account

+ +
+
+ + setNewAccount({ ...newAccount, name: e.target.value }) + } + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" /> - ))} + + {newAccount.type === "savings" && ( + + )} + {newAccount.type === "credit_card" && ( + + )} + handleCurrencyChange(e, setNewBalance)} + onBlur={(e) => handleCurrencyBlur(e, newBalance, setNewBalance)} + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" + /> +
+
+ +
+ )} -
- + {/* Accounts Table */} +
+ {/* Header */} +
+
Account Name
+
Type
+
Balance
+
Last Updated
+
-
- {seen ? ( - - ) : null} - + {/* Body */} +
+
+ {filteredAccounts.map((account, index) => { + const isExpanded = account.isExpanded; + const editValues = editingValues[account.id] || {}; + + return ( +
+ {/* Main Row */} +
toggleExpand(account.id)} + > +
+ {account.name} +
+
+ {account.type ? account.type.replace("_", " ") : ""} +
+
+ {formatCurrency(account.balance)} +
+
+ {formatDate(account.last_updated)} +
+
+ +
+
+ + {/* Expanded Edit Row */} + {isExpanded && ( +
+
+
+ + + handleFieldChange( + account.id, + "name", + e.target.value, + ) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+
+ + +
+ + {account.subtype && ( +
+ + {account.type === "savings" && ( + + )} + {account.type === "credit_card" && ( + + )} +
+ )} +
+ + + handleListCurrencyChange( + e, + handleFieldChange, + account.id, + "balance", + ) + } + onBlur={(e) => + handleListCurrencyBlur( + e, + handleFieldChange, + account.id, + "balance", + ) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+
+
+ + +
+
+ )} +
+ ); + })} +
+
+
+
); } + +export default AccountList; diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx deleted file mode 100644 index 7987a6d..0000000 --- a/app/client/src/components/AccountListCard.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { deleteUserAccount } from "../api/Account.ts"; -import { useState } from "react"; -import AccountPopup from "./AccountForm.tsx"; -import { type Account } from "../types/AccountType.ts"; -import type { AuthSession } from "@/types/authTypes.ts"; -import { accountCategory } from "@/enum/AccountCategory.ts"; - -interface cardProp { - account: Account; - session: AuthSession; - setAccount: (editAccount: Account) => void; - removeAccount: (removeAccount: Account) => void; -} - -export default function Card({ - account, - session, - setAccount, - removeAccount, -}: cardProp) { - //Stores the value that toggles thhe account popup form - const [seen, setSeen] = useState(false); - - const toggle = () => { - setSeen(!seen); - }; - - //Deletes the user account from the server and then remove it from the list - const deleteAccount = () => { - try { - //Send a request to delete user account - deleteUserAccount(session, account).then((result) => { - //Sucessfully deleted account - if (result) { - removeAccount(account); - } - }); - } catch { - alert("Failed to delete account " + account.name); - } - }; - - return ( - <> -
-

{account.name}

-

- -
-

- {accountCategory[account.type as keyof typeof accountCategory] + - " " + - (account.subtype ? " " + account.subtype : "")} -

-

{Number(account.balance).toFixed(2)}

-
- -
- - -
-
- {seen ? ( - - ) : null} - - ); -} diff --git a/app/client/src/components/CSVImportModal.tsx b/app/client/src/components/CSVImportModal.tsx new file mode 100644 index 0000000..10c0553 --- /dev/null +++ b/app/client/src/components/CSVImportModal.tsx @@ -0,0 +1,73 @@ +import { useState } from "react"; +import { AiOutlineClose } from "react-icons/ai"; +import CsvUpload from "./csvread/CsvUpload"; +// import type { AuthSession } from "@/types/authTypes"; +import type { Transaction } from "@/types/Transaction"; + +interface CSVImportModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: (newTransactions: Transaction[]) => void; + accountId: number; +} + +function CSVImportModal({ + isOpen, + onClose, + onSuccess, + accountId, +}: CSVImportModalProps) { + const [importComplete, setImportComplete] = useState(false); + + if (!isOpen) return null; + + const handleImported = (newTransactions: Transaction[]) => { + onSuccess(newTransactions); + setImportComplete(true); + }; + + const handleClose = () => { + setImportComplete(false); + onClose(); + }; + + return ( +
+
+ {/* Header */} +
+

+ Import Transactions from CSV +

+ +
+ + {/* Body */} +
+ {importComplete ? ( +
+
+ ✓ Import Complete! +
+ +
+ ) : ( + + )} +
+
+
+ ); +} + +export default CSVImportModal; diff --git a/app/client/src/components/SankeyChart.tsx b/app/client/src/components/SankeyChart.tsx index 6ed3032..3eefad8 100644 --- a/app/client/src/components/SankeyChart.tsx +++ b/app/client/src/components/SankeyChart.tsx @@ -68,7 +68,7 @@ function CustomNode({ x, y, width, height, index, payload }: SankeyNodeProps) { fontSize="14" fill="#ffffff" > - {payload.name} + {formatCategoryLabel(payload.name)} word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); +} + export default SankeyChart; diff --git a/app/client/src/components/TransactionCard.tsx b/app/client/src/components/TransactionCard.tsx deleted file mode 100644 index 4e4c600..0000000 --- a/app/client/src/components/TransactionCard.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useState } from "react"; -import { deleteTransaction } from "../api/Transaction"; -import { type Transaction } from "../types/Transaction"; -import TransactionPopup from "./TransationForm"; -import type { AuthSession } from "@/types/authTypes"; - -interface cardProp { - transaction: Transaction; - session: AuthSession; - setTransaction: (editedTransaction: Transaction) => void; - removeTransaction: (removeTransaction: Transaction) => void; -} - -export default function Card({ - transaction, - session, - setTransaction, - removeTransaction, -}: cardProp) { - const [seen, setSeen] = useState(false); - - const toggle = () => { - setSeen(!seen); - }; - - //Deletes the user account from the server and then remove it from the list - const deleteTrans = () => { - try { - //Send a request to delete account's transaction - deleteTransaction(session, transaction).then((result) => { - //Sucessfully deleted transaction - if (result) { - removeTransaction(transaction); - } - }); - } catch { - alert("Failed to delete transaction " + transaction.id); - } - }; - - return ( - <> -
-

{transaction.id}

-
-

{"$" + transaction.amount.toFixed(2)}

-

{"Date:" + transaction.date}

-

{"Type: " + transaction.category}

-

-

{transaction.description}

-
- -
- - -
-
- - {seen ? ( - - ) : null} - - ); -} diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx deleted file mode 100644 index 8e6fef8..0000000 --- a/app/client/src/components/TransactionList.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useEffect, useState } from "react"; -import { getTransactions } from "../api/Transaction"; -import { type Transaction } from "../types/Transaction"; -import TransactionPopup from "./TransationForm"; -import TransactionCard from "./TransactionCard"; -import type { AuthSession } from "@/types/authTypes"; -import { type Account } from "@/types/AccountType"; -import { getUserAccounts } from "@/api/Account"; -import SelectAccount from "./SelectAccount"; - -interface listProp { - session: AuthSession; -} - -export default function TransactionList({ session }: listProp) { - const [userAccounts, setUserAccounts] = useState([]); - const [selectedAccount, setSelectedAccount] = useState(0); - const [accountTransactions, setAccountTransactions] = useState( - [], - ); - const [seen, setSeen] = useState(false); - - useEffect(() => { - getUserAccounts(session) - .then((accounts) => { - console.log(accounts); - - //Detemrine accounts exist - if (accounts) { - setUserAccounts(accounts); - } else { - //alert("Failed to get accounts"); - } - }) - .catch(() => { - //alert("Failed to get accounts"); - }); - }, [session, selectedAccount]); - - //Try to get the account's transaction from the server - useEffect(() => { - if (selectedAccount) { - getTransactions(session, selectedAccount) - .then((transactions) => { - //Determine if we acquired the accounts transaction - if (transactions) { - setAccountTransactions(transactions); - } - }) - .catch(() => { - alert("Failed to get transaction"); - }); - } - }, [session, selectedAccount]); - - //Adds a account to the list - const addTransaction = (newTransaction: Transaction) => { - setAccountTransactions((userTransactions) => [ - ...userTransactions, - newTransaction, - ]); - }; - - //Removes any transaction from the list thats shares the same id - const removeTransaction = (remove: Transaction) => { - setAccountTransactions((userTransactions) => - userTransactions.filter((transaction) => transaction.id !== remove.id), - ); - }; - - const setTransaction = (editTransaction: Transaction) => { - setAccountTransactions( - accountTransactions.map((transaction) => - transaction.id === editTransaction.id - ? { ...editTransaction } - : transaction, - ), - ); - }; - - const toggle = () => { - setSeen(!seen); - }; - - return ( - <> -
-
- , - ) => setSelectedAccount(Number(event.target.value))} - /> -
- -
- {accountTransactions.map((transaction) => ( - - ))} -
- -
- -
-
- - {seen ? ( - - ) : null} - - ); -} diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index 3070169..98af1de 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -1,115 +1,715 @@ -import { getTransactions } from "@/api/DashboardAPI"; -import type { Transaction } from "@/types/Transaction"; -import { useEffect, useState } from "react"; -import { AiOutlineTransaction } from "react-icons/ai"; +import { useState, useEffect, useMemo } from "react"; +import { getAccountIdsForUser, getTransactions } from "../api/DashboardAPI"; +import { + deleteTransaction, + updateTransaction, + createTransaction, +} from "../api/DashboardAPI.ts"; +import type { Transaction } from "../types/Transaction"; +import { + AiOutlineTransaction, + AiOutlineSearch, + AiOutlinePlus, + AiOutlineClose, + AiOutlineUpload, +} from "react-icons/ai"; +import { FiEdit2 } from "react-icons/fi"; import NoItemState from "./NoItemState"; +import type { MinimizedAccount } from "@/types/AccountType.ts"; +import CSVImportModal from "./CSVImportModal"; -function TransactionTable() { - const [transactions, setTransactions] = useState(null); +interface TransactionTableProps { + initialLimit?: number; + loadMoreIncrement?: number; +} + +// interface EditableTransaction extends Transaction { +// } +function TransactionTable({ + initialLimit = 100, + loadMoreIncrement = 50, +}: TransactionTableProps) { + const [allTransactions, setAllTransactions] = useState([]); + const [displayLimit, setDisplayLimit] = useState(initialLimit); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [showAddForm, setShowAddForm] = useState(false); + const [editingValues, setEditingValues] = useState<{ + [key: number]: Partial; + }>({}); + const [userAccounts, setUserAccounts] = useState([]); + const [selectedAccountId, setSelectedAccountId] = useState(""); + const [showImportModal, setShowImportModal] = useState(false); + const [selectedAccountForImport, setSelectedAccountForImport] = useState< + number | null + >(null); + const [expandedRowId, setExpandedRowId] = useState(null); + + // Fetch all transactions on component load useEffect(() => { const fetchTransactions = async () => { try { + setIsLoading(true); const txs = await getTransactions(); - //console.log("Fetched transactions:", txs); - setTransactions(txs); + setAllTransactions(txs || []); } catch (error) { console.error("Error fetching transactions:", error); + } finally { + setIsLoading(false); } }; fetchTransactions(); }, []); - const noTransactionFound = ( - } - /> - ); + // Fetch accounts for dropdown + useEffect(() => { + const fetchAccounts = async () => { + try { + const accounts = await getAccountIdsForUser(); + if (accounts && accounts.length > 0) { + setUserAccounts(accounts); + setSelectedAccountId(accounts[0].id); + } + } catch (error) { + console.error("Error fetching accounts:", error); + } + }; + fetchAccounts(); + }, []); + + const handleImportSuccess = (newTransactions: Transaction[]) => { + setAllTransactions((prev) => [...newTransactions, ...prev]); + setShowImportModal(false); + setSelectedAccountForImport(null); + }; + + //helper function for date formatting + const formatDateForInput = (dateStr: string): string => { + if (!dateStr) return ""; + // If it's already YYYY-MM-DD, return as is + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr; + // Otherwise parse and format + return new Date(dateStr).toISOString().split("T")[0]; + }; + + const handleRowClick = (transactionId: number) => { + const isExpanding = expandedRowId !== transactionId; + setExpandedRowId(isExpanding ? transactionId : null); + + // Initialize editing values when expanding + if (isExpanding) { + const tx = allTransactions.find((t) => t.id === transactionId); + if (tx && !editingValues[transactionId]) { + setEditingValues((prev) => ({ + ...prev, + [transactionId]: { + date: tx.date, + description: tx.description || "", + category: tx.category, + amount: tx.amount, + account_name: tx.account_name, + sender: tx.sender || "", + recipient: tx.recipient || "", + }, + })); + } + } + }; + + // client-side search + const filteredTransactions = useMemo(() => { + if (!searchTerm.trim()) return allTransactions; + + const term = searchTerm.toLowerCase(); + return allTransactions.filter( + (tx) => + tx.description?.toLowerCase().includes(term) || + tx.category?.toLowerCase().includes(term) || + tx.sender?.toLowerCase().includes(term) || + tx.recipient?.toLowerCase().includes(term) || + tx.account_name?.toLowerCase().includes(term) || + new Date(tx.date).toLocaleDateString().includes(term) || + tx.amount.toString().includes(term), + ); + }, [allTransactions, searchTerm]); + + const displayedTransactions = filteredTransactions.slice(0, displayLimit); + const hasMore = displayLimit < filteredTransactions.length; + + // Format helpers + const formatAmount = (amount: number): string => { + return amount < 0 + ? `-$${Math.abs(amount).toFixed(2)}` + : `$${amount.toFixed(2)}`; + }; + + const formatCategoryLabel = (category: string): string => { + return category + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); + }; + + // handle field changes in expanded edit form + const handleFieldChange = ( + transactionId: number, + field: string, + value: string | number, + ) => { + setEditingValues((prev) => ({ + ...prev, + [transactionId]: { + ...prev[transactionId], + [field]: value, + }, + })); + }; + + // save edited transaction + const handleSaveEdit = async (transactionId: number) => { + const updates = editingValues[transactionId]; + if (!updates) return; + + try { + const success = await updateTransaction({ + id: transactionId, + ...updates, + } as Transaction); + + if (!success) throw new Error("Update failed"); + + const isExpanding = expandedRowId !== transactionId; + setExpandedRowId(isExpanding ? transactionId : null); + + const freshTransactions = await getTransactions(); + setAllTransactions(freshTransactions || []); + + setEditingValues((prev) => { + const newState = { ...prev }; + delete newState[transactionId]; + return newState; + }); + + // close the expanded row + setExpandedRowId(null); + } catch (error) { + console.error("Failed to update transaction:", error); + alert("Failed to update transaction"); + } + }; + + // delete transaction + const handleDelete = async (transactionId: number) => { + if (!confirm("Are you sure you want to delete this transaction?")) return; + + try { + await deleteTransaction(transactionId); + setAllTransactions((prev) => + prev.filter((tx) => tx.id !== transactionId), + ); + } catch (error) { + console.error("Failed to delete transaction:", error); + alert("Failed to delete transaction"); + } + }; + + //add new transaction state + const [newTransaction, setNewTransaction] = useState>({ + date: new Date().toISOString().split("T")[0], + description: "", + category: "", + amount: 0, + sender: "", + recipient: "", + }); + + const handleAddTransaction = async () => { + if ( + !newTransaction.date || + !newTransaction.category || + newTransaction.amount === undefined + ) { + alert("Please fill in required fields (date, category, amount)"); + return; + } - const transactionTable = - transactions && transactions.length > 0 ? ( -
+ if (!selectedAccountId) { + alert("Please select an account"); + return; + } + + try { + const created = await createTransaction({ + ...newTransaction, + financialAccount_id: selectedAccountId as number, + } as Transaction); + + const selectedAccount = userAccounts.find( + (acc) => acc.id === selectedAccountId, + ); + + const transactionWithAccountName = { + ...created, + account_name: selectedAccount?.name || "Unknown", + }; + + setAllTransactions((prev) => [transactionWithAccountName, ...prev]); + setNewTransaction({ + date: new Date().toISOString().split("T")[0], + description: "", + category: "", + amount: 0, + sender: "", + recipient: "", + }); + setSelectedAccountId(userAccounts[0]?.id || ""); + setShowAddForm(false); + + if (expandedRowId !== null) { + setExpandedRowId(null); + } + } catch (error) { + console.error("Failed to create transaction:", error); + alert("Failed to create transaction"); + } + }; + + if (isLoading) { + return ( +
+
Loading transactions...
+
+ ); + } + + if (allTransactions.length === 0) { + return ( + } + /> + ); + } + + return ( +
+ {/* Search Bar */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-black/50 border border-green-500/20 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-green-500" + /> +
+
+ + {/* Import CSV Modal */} +
+ +
+ {showImportModal && selectedAccountForImport && ( + { + setShowImportModal(false); + setSelectedAccountForImport(null); + }} + onSuccess={handleImportSuccess} + accountId={selectedAccountForImport} + /> + )} +
+
+ + {/* Add Transaction Form */} + {showAddForm && ( +
+
+

New Transaction

+ +
+
+ + setNewTransaction({ ...newTransaction, date: e.target.value }) + } + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" + /> + + setNewTransaction({ + ...newTransaction, + description: e.target.value, + }) + } + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" + /> + + setNewTransaction({ + ...newTransaction, + category: e.target.value, + }) + } + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" + /> + + setNewTransaction({ + ...newTransaction, + amount: parseFloat(e.target.value), + }) + } + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" + /> + + {/* Account Selector */} + + + + setNewTransaction({ ...newTransaction, sender: e.target.value }) + } + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" + /> + + setNewTransaction({ + ...newTransaction, + recipient: e.target.value, + }) + } + className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" + /> +
+
+ +
+
+ )} + + {/* Transactions Table */} +
{/* Header */} -
+
Date
Description
Category
Amount
+
Account
From
To
+
{/* Body */}
- {transactions.map((tx, index) => ( -
- {/* Date */} -
- {new Date(tx.date).toLocaleDateString()} -
+ {displayedTransactions.map((tx, index) => { + const isExpanded = expandedRowId === tx.id; + const editValues = editingValues[tx.id] || {}; + const currentAccountId = + editValues.financialAccount_id || + userAccounts.find((acc) => acc.name === tx.account_name)?.id || + ""; - {/* Description */} -
- {tx.description || "N/A"} -
+ return ( +
+ {/* Main Row */} +
handleRowClick(tx.id)} + > +
+ {formatDateForInput(tx.date)} +
+
+ {tx.description || "N/A"} +
+
+ {formatCategoryLabel(tx.category)} +
+
+ {formatAmount(tx.amount)} +
+
+ {tx.account_name || "Unknown Account"} +
+
+ {tx.sender || "-"} +
+
+ {tx.recipient || "-"} +
+
+ +
+
- {/* Category */} -
- {formatCategoryLabel(tx.category)} -
- - {/* Amount */} -
- {tx.amount < 0 - ? `-$${Math.abs(tx.amount).toFixed(2)}` - : `$${tx.amount.toFixed(2)}`} -
+ {/* Expanded Edit Row */} + {isExpanded && ( +
+
+
+ + + handleFieldChange(tx.id, "date", e.target.value) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+
+ + + handleFieldChange( + tx.id, + "description", + e.target.value, + ) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+
+ + + handleFieldChange( + tx.id, + "category", + e.target.value, + ) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+
+ + + handleFieldChange( + tx.id, + "amount", + parseFloat(e.target.value), + ) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
- {/* From */} -
{tx.sender}
+ {/* Account Selector for Edit Form */} +
+ + +
- {/* To */} -
{tx.recipient}
-
- ))} +
+ + + handleFieldChange(tx.id, "sender", e.target.value) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+
+ + + handleFieldChange( + tx.id, + "recipient", + e.target.value, + ) + } + className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+
+
+ + +
+
+ )} +
+ ); + })}
- ) : null; - return transactions && transactions.length === 0 - ? noTransactionFound - : transactionTable; -} -//split on underscore, capitalize first letter of each word, then join with space -function formatCategoryLabel(category: string): string { - return category - .split("_") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(" "); + {/* Load More Button */} + {hasMore && ( +
+ +
+ )} +
+ ); } export default TransactionTable; diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx deleted file mode 100644 index 6c0609d..0000000 --- a/app/client/src/components/TransationForm.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import { useEffect, useState } from "react"; -import CsvUpload from "./csvread/CsvUpload"; -import "./userForm.css"; -import { getUserAccounts } from "../api/Account"; -import { validateTransactionForm } from "../utils/ValidateForms"; -import { postTranscations, putTranscations } from "../api/Transaction"; -import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput"; -import { type Transaction } from "../types/Transaction"; -import { type Account } from "@/types/AccountType"; -import { transactionCategory } from "@/enum/TransactionCategory"; -import type { AuthSession } from "@/types/authTypes"; -import SelectAccount from "@/components/SelectAccount"; - -interface popupProp { - toggle: () => void; - session: AuthSession; - setTransaction?: (editedTransaction: Transaction) => void; - addTransaction?: (newTransaction: Transaction) => void; - edit: boolean; - selectedTransaction?: Transaction; -} - -//Gets the keys of the enum -type typeOfTransaction = keyof typeof transactionCategory; - -//Returns a form of for the user to enter their info -export default function PopupForm({ - toggle, - session, - setTransaction, - addTransaction, - edit, - selectedTransaction, -}: popupProp) { - //Handles the submiting the form - const handleSubmit = () => { - const transferAmount = Number(amount); - let userTransaction: Transaction; - let recipient = ""; - let sender = ""; - - if ( - selectedType && - validateTransactionForm( - selectedAccount, - selectedType, - transferAmount, - selectedDate, - undefined, - ) - ) { - let target; - - if (account) { - if (selectedType === transactionCategory.INCOME) { - target = account.find( - (account) => account.id === selectedAccount, - )?.name; - if (target) { - recipient = target; - } - - sender = other; - } else { - recipient = other; - - target = account.find( - (account) => account.id === selectedAccount, - )?.name; - if (target) { - sender = target; - } - } - } - - userTransaction = { - id: 0, - financialAccount_id: Number(selectedAccount), - recipient: recipient, - sender: sender, - amount: transferAmount, - category: selectedType, - date: new Date(selectedDate), - }; - - if (edit && selectedTransaction) { - try { - //Editing selected transaction - userTransaction.id = selectedTransaction.id; - - //Send a request to update the transaction - putTranscations(session, userTransaction).then((result) => { - //Determine if we're able to able to edit the transaction - if (result && setTransaction) { - setTransaction(userTransaction); - } - }); - } catch { - alert("Failed to edit transaction"); - } - } else { - try { - //Send a request to create the transaction - postTranscations(session, userTransaction).then((response) => { - //Successful put if response is returned - if (response && response.id) { - userTransaction.id = response.id; - - if (addTransaction) { - addTransaction(userTransaction); - } - } - }); - } catch { - alert("Failed to create transaction"); - } - } - - //Need to insert function that sets the state in parent component - - toggle(); - } else { - alert("Please fill out the transaction form"); - } - }; - - //Handle when the user adds a file to the form - /*const handleFile = (event:React.ChangeEvent) =>{ - - if(event.target && event.target.files && event.target.files[0]){ - setFile(event.target.files[0]) - } else { - setFile(undefined) - } - - //Insert cvs parsing function/validation function - }*/ - - //State of the user's account - const [account, setAccount] = useState(); - - useEffect(() => { - getUserAccounts(session) - .then((accounts) => { - console.log(accounts); - //Detemrine accounts exist - if (accounts) { - setAccount(accounts); - } else { - alert( - "Failed to retrieve user's accounts, cannot make a transaction", - ); - //toggle(); - } - }) - .catch(() => { - alert("Failed to retrieve user's accounts, cannot make a transaction"); - //toggle(); - }); - }, [session]); - - //Holds state of user input - const [selectedAccount, setSelectedAccount] = useState(() => { - if (edit && selectedTransaction) { - return selectedTransaction.financialAccount_id; - } else { - return 0; - } - }); - - const [selectedType, setSelectedType] = useState(() => { - if (edit && selectedTransaction) { - return selectedTransaction.category; - } else { - return ""; - } - }); - - const [amount, setAmount] = useState(() => { - if (edit && selectedTransaction) { - return selectedTransaction.amount.toFixed(2); - } else { - return ""; - } - }); - //const [file, setFile] = useState(undefined) - - const [selectedDate, setSelectedDate] = useState(() => { - if (edit && selectedTransaction) { - return new Date(selectedTransaction.date).toISOString().slice(0, 10); - } else { - //Default - return ""; - } - }); - - const [other, setOther] = useState(() => { - if (edit && selectedTransaction) { - if (selectedType === transactionCategory.INCOME) { - return selectedTransaction.sender; - } else { - return selectedTransaction.recipient; - } - } else { - //Default - return ""; - } - }); - - //Holds the types of transfers - const transCat: typeOfTransaction[] = Object.keys( - transactionCategory, - ) as typeOfTransaction[]; - - return ( - <> -
-
- {edit ? ( -

Edit Transaction

- ) : ( -

Create Transaction

- )} - - {account && ( - , - ) => setSelectedAccount(Number(event.target.value))} - /> - )} - -

- - {selectedType == transactionCategory.INCOME ? ( - - ) : ( - - )} - setOther(event.target.value)} - > -

- - - -

- - - handleCurrencyChange(event, setAmount)} - onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} - placeholder="0.00" - /> -

- - - setSelectedDate(event.target.value)} - /> -

- - {edit ? null : ( - <> - - {!edit && selectedAccount && ( - { - if (addTransaction) { - newTxs.forEach((tx) => addTransaction(tx)); - } - }} - /> - )} - - )} -

- -
- - {edit ? ( - - ) : ( - - )} -
-
-
- - ); -} diff --git a/app/client/src/components/csvread/CsvConfirm.tsx b/app/client/src/components/csvread/CsvConfirm.tsx index fb9e52f..2edcadb 100644 --- a/app/client/src/components/csvread/CsvConfirm.tsx +++ b/app/client/src/components/csvread/CsvConfirm.tsx @@ -22,7 +22,7 @@ export default function CsvConfirmation({ rows, onConfirm, onBack }: Props) { .reduce((sum, r) => sum + (r.amount ?? 0), 0); return ( -
+

Summary

diff --git a/app/client/src/components/csvread/CsvDrop.tsx b/app/client/src/components/csvread/CsvDrop.tsx index e6a2dc4..a0c24e3 100644 --- a/app/client/src/components/csvread/CsvDrop.tsx +++ b/app/client/src/components/csvread/CsvDrop.tsx @@ -1,4 +1,5 @@ //component to upload for CSV files +// import { color } from "chart.js/helpers"; import React, { useState } from "react"; interface CsvDropProps { @@ -31,10 +32,10 @@ export default function CsvDrop({ onFileSelect }: CsvDropProps) { background: isDragging ? "#eef" : "#fafafa", cursor: "pointer", marginBottom: "1rem", + color: "#888", }} > -

Drag & drop your CSV here, or click to select

- n +

Drag & drop your CSV here, or click to select a file

+
Total rows: {rows.length}
Valid rows: {validCount}
diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx index 3fe42d1..97de7c6 100644 --- a/app/client/src/components/csvread/CsvUpload.tsx +++ b/app/client/src/components/csvread/CsvUpload.tsx @@ -10,16 +10,16 @@ import { parseCsvFile } from "../../utils/ParseCsv"; import type { TransactionDraft } from "../../utils/ConvertTransaction"; import SuccessScreen from "./csvSuccess"; import { uploadCsvTransactions } from "@/api/Transaction"; -import type { AuthSession } from "@/types/authTypes"; +// import type { AuthSession } from "@/types/authTypes"; import type { Transaction } from "@/types/Transaction"; export default function CsvUpload({ accountId, - session, + // session, onImported, }: { accountId: number; - session: AuthSession; + // session: AuthSession; onImported: (txs: Transaction[]) => void; }) { const [file, setFile] = useState(null); // selected CSV file @@ -104,7 +104,7 @@ export default function CsvUpload({ try { const response = await uploadCsvTransactions( - session, + // session, accountId, parsedData, ); @@ -129,7 +129,7 @@ export default function CsvUpload({ }; return ( -
+
{importResult && (
); } diff --git a/app/client/src/pages/ProjectionPage.tsx b/app/client/src/pages/ProjectionPage.tsx index 1a63e8e..0d37324 100644 --- a/app/client/src/pages/ProjectionPage.tsx +++ b/app/client/src/pages/ProjectionPage.tsx @@ -22,7 +22,7 @@ import { type projectedDataResponse, } from "@/types/responseTypes"; import ProjectionGraph from "@/components/ProjectionGraph"; -import { TbGraph } from "react-icons/tb"; +import { TbGraph, TbCalculator, TbRefresh } from "react-icons/tb"; import NoItemState from "@/components/NoItemState"; import { handleCurrencyChange, @@ -65,34 +65,31 @@ function ProjectionPage({ session }: ProjectionProp) { const [amount, setAmount] = useState(""); const [period, setPeriod] = useState(""); const [minPay, setMinPay] = useState(""); - const [selectedType, setSelectedType] = useState<"Saving" | "Debt">("Saving"); - const [savingData, setSavingData] = useState< projectedDataResponse | undefined >(undefined); const [debtData, setDebtData] = useState( undefined, ); - const [debtRequest, setDebtRequest] = useState< projectionDebtRequest | undefined >(undefined); const [savingRequest, setSavingRequest] = useState< projectionSavingRequest | undefined >(undefined); - - //Total amount of interest generated const [totalInterest, setTotalInterest] = useState(0); - //Total amoutn of pay const [totalPay, setTotalPay] = useState(0); + const [isLoading, setIsLoading] = useState(false); - const calculateProjection = () => { + const calculateProjection = async () => { const inputAmount = Number(amount); const inputMinPay = Number(minPay); const inputInterest = Number(interest); const inputPeriod = Number(period); + setIsLoading(true); + switch (selectedType) { case "Debt": if ( @@ -117,37 +114,35 @@ function ProjectionPage({ session }: ProjectionProp) { setDebtRequest(newDebtRequest); - getDebtProjection(session, newDebtRequest) - .then((data) => { + try { + const data = await getDebtProjection(newDebtRequest); + if (data) { let dataTotalInterest = 0; let dataTotalPay = 0; const debtLineData: LineInfo = { name: "Remaining Debt", data: [], }; - console.log(data); - if (data) { - const graphData: projectedDataResponse = { - lineInfo: [], - dateLabel: [], - }; - data.debtStages.map((stage) => { - debtLineData.data.push(stage.remainingDebt); - graphData.dateLabel.push(stage.installmentDate); - dataTotalInterest += stage.interestAmount; - dataTotalPay += stage.principalAmount; - }); - - graphData.lineInfo = [debtLineData]; - - setDebtData(graphData); - setTotalPay(dataTotalPay); - setTotalInterest(dataTotalInterest); - } - }) - .catch((error) => { - alert(error); - }); + const graphData: projectedDataResponse = { + lineInfo: [], + dateLabel: [], + }; + + data.debtStages.map((stage) => { + debtLineData.data.push(stage.remainingDebt); + graphData.dateLabel.push(stage.installmentDate); + dataTotalInterest += stage.interestAmount; + dataTotalPay += stage.principalAmount; + }); + + graphData.lineInfo = [debtLineData]; + setDebtData(graphData); + setTotalPay(dataTotalPay); + setTotalInterest(dataTotalInterest); + } + } catch { + alert("Failed to get debt projection"); + } } else { alert("Please enter all fields"); } @@ -173,52 +168,55 @@ function ProjectionPage({ session }: ProjectionProp) { setSavingRequest(newSavingRequest); - getSavingProjection(session, newSavingRequest) - .then((data) => { - console.log(data); + try { + const data = await getSavingProjection(newSavingRequest); + if (data) { const graphData: projectedDataResponse = { dateLabel: [], lineInfo: [], }; - const bestCase: LineInfo = { name: "Best Case", data: [] }; + const bestCase: LineInfo = { + name: "Best Case (Optimistic)", + data: [], + }; const expectedCase: LineInfo = { name: "Expected Case", data: [], }; - const worstCase: LineInfo = { name: "Worst Case", data: [] }; - - if (data) { - //Extract all the data from the response - data.map((datapoint) => { - graphData.dateLabel.push(datapoint.date); - bestCase.data.push(datapoint.accumulative_best_balance); - expectedCase.data.push( - datapoint.accumulative_expected_balance, - ); - worstCase.data.push(datapoint.accumulative_worst_balance); - }); - - graphData.lineInfo = [bestCase, expectedCase, worstCase]; - setSavingData(graphData); - } else { - alert("Failed to get the projection "); - } - }) - .catch(() => { - alert("Failed to get the projection"); - }); + const worstCase: LineInfo = { + name: "Worst Case (Conservative)", + data: [], + }; + + data.map((datapoint) => { + graphData.dateLabel.push(datapoint.date); + bestCase.data.push(datapoint.accumulative_best_balance); + expectedCase.data.push(datapoint.accumulative_expected_balance); + worstCase.data.push(datapoint.accumulative_worst_balance); + }); + + graphData.lineInfo = [bestCase, expectedCase, worstCase]; + setSavingData(graphData); + } + } catch { + alert("Failed to get saving projection"); + } } else { alert("Please enter all fields"); } + break; } + + setIsLoading(false); }; const handleTypeChange = (typeChange: "Saving" | "Debt") => { setSelectedType(typeChange); + setSavingData(undefined); + setDebtData(undefined); - if (selectedType === "Debt") { + if (typeChange === "Debt") { if (debtRequest) { - //Set the fields to what was used in the debt projection graph setSelectedAccount(Number(debtRequest.id)); setAmount(debtRequest.remainingAmount.toFixed(2)); setInterest(debtRequest.interestRate.toString()); @@ -233,16 +231,14 @@ function ProjectionPage({ session }: ProjectionProp) { setPeriod(""); setNextDueDate(""); } - } else if (selectedType === "Saving") { + } else if (typeChange === "Saving") { if (savingRequest) { - //Assigns the fields to what the projection of the saving account used setSelectedAccount(savingRequest.financial_account_id); - setAmount((savingRequest.balance * 100).toFixed(2)); + setAmount(savingRequest.balance.toFixed(2)); setInterest(savingRequest.annual_interest_rate.toString()); setMinPay(savingRequest.monthly_deposit.toFixed(2)); setPeriod(savingRequest.time_frame.toString()); } else { - //Resets the fields setSelectedAccount(0); setAmount(""); setInterest(""); @@ -252,256 +248,275 @@ function ProjectionPage({ session }: ProjectionProp) { } }; - //Onchange handler for the select accounts const handleSelectAccount = (event: React.ChangeEvent) => { - let target; const id = Number(event.target.value); if (selectedType === "Debt") { setSelectedDebt(id); - - target = debts.find((account) => account.id === id)?.balance; + const target = debts.find((account) => account.id === id)?.balance; + setAmount(target ? target.toFixed(2) : "0"); } else if (selectedType === "Saving") { setSelectedAccount(id); - - target = accounts.find((account) => account.id === id)?.balance; - } - - if (target) { - setAmount(target.toFixed(2)); - } else { - setAmount("0"); + const target = accounts.find((account) => account.id === id)?.balance; + setAmount(target ? target.toFixed(2) : "0"); } }; - //Get the saving accounts useEffect(() => { if (selectedType === "Saving") { - getSaving(session) - .then((userAccounts) => { - console.log(userAccounts); - - //Detemrine accounts exist - if (userAccounts) { - setAccounts(userAccounts); - } - }) - .catch(() => { - //alert("Failed to get accounts"); - }); + getSaving(session).then((userAccounts) => { + if (userAccounts) setAccounts(userAccounts); + }); } }, [session, selectedType]); - //Gets the debt accounts useEffect(() => { if (selectedType === "Debt") { - getDebt(session) - .then((userDebts) => { - console.log(userDebts); - - //Detemrine if there are any debts - if (userDebts) { - setDebts(userDebts); - } - }) - .catch(() => {}); + getDebt().then((userDebts) => { + if (userDebts) setDebts(userDebts); + }); } }, [session, selectedType]); - const typeButton = [ - { key: "Saving", label: "Saving" }, - { key: "Debt", label: "Debt" }, - ].map(({ key, label }) => ( - - )); + const typeButtons = [ + { key: "Saving", label: "Savings Projection" }, + { key: "Debt", label: "Debt Payoff Projection" }, + ]; const glowLeft = ( -
+
); const glowRight = (
); + return ( -
+
{glowLeft} {glowRight} -

Projection

-

- Here you can view a projection of you're saving's or debt -

- -

Select Saving Account or Debt

-
- {typeButton} -
+
+

Financial Projections

+

+ Plan your financial future with savings growth and debt payoff + simulations +

+ + {/* Type Selector */} +
+ {typeButtons.map(({ key, label }) => ( + + ))} +
-
- {selectedType === "Saving" ? ( - - ) : ( - - )} -
- <> - - handleCurrencyChange(event, setAmount)} - onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} - /> - - - handleCurrencyChange(event, setMinPay)} - onBlur={(event) => handleCurrencyBlur(event, minPay, setMinPay)} - /> - -
- -
- {selectedType === "Saving" ? ( - savingData ? ( - <> - - - ) : ( - <> + {/* Results Section */} +
+

+ {selectedType === "Saving" + ? "Savings Growth Projection" + : "Debt Payoff Schedule"} +

+ + {selectedType === "Saving" ? ( + savingData ? ( + <> + + + ) : ( } /> - - ) - ) : null} - - {selectedType === "Debt" ? ( - debtData ? ( - <> - -
- {"Total Amount Paid:$" + - totalPay.toFixed(2) + - " Total Interest Paid:$" + - totalInterest.toFixed(2)} -
- - ) : ( - <> + ) + ) : null} + + {selectedType === "Debt" ? ( + debtData ? ( + <> + +
+
+
+

Total Amount Paid

+

+ ${totalPay.toFixed(2)} +

+
+
+

+ Total Interest Paid +

+

+ ${totalInterest.toFixed(2)} +

+
+
+
+ + ) : ( } /> - - ) - ) : null} + ) + ) : null} +
); diff --git a/app/client/src/types/AccountType.ts b/app/client/src/types/AccountType.ts index 0fcbbed..341d9b1 100644 --- a/app/client/src/types/AccountType.ts +++ b/app/client/src/types/AccountType.ts @@ -6,5 +6,10 @@ export interface Account { balance: number; value: number; subtype?: string; - last_updated: Date; + last_updated: string; +} + +export interface MinimizedAccount { + id: number; + name: string; } diff --git a/app/client/src/types/Transaction.ts b/app/client/src/types/Transaction.ts index 1620df8..528e9fd 100644 --- a/app/client/src/types/Transaction.ts +++ b/app/client/src/types/Transaction.ts @@ -3,8 +3,9 @@ export type Transaction = { financialAccount_id: number; amount: number; category: string; - date: Date; // ISO format date string + date: string; // ISO format date string - used to be Date sender: string; recipient: string; description?: string; + account_name?: string; }; diff --git a/app/client/src/utils/handleInput.ts b/app/client/src/utils/handleInput.ts index 0b0a071..4b79e95 100644 --- a/app/client/src/utils/handleInput.ts +++ b/app/client/src/utils/handleInput.ts @@ -6,10 +6,8 @@ export const handleCurrencyChange = ( setCurrency: React.Dispatch>, ): void => { let input = event.target.value; - const pattern = /^\d*\.?\d{0,2}$/; + const pattern = /^-?\d*\.?\d{0,2}$/; - console.log(input); - console.log(pattern.test(input)); //Determine if the input follows the format/pattern if (pattern.test(input)) { input = input.replace(/^0+(?=\d)/, ""); @@ -57,3 +55,39 @@ export const handleNumberChange = ( setNumber(changeNumber.toString()); } }; + +export const handleListCurrencyChange = ( + event: React.ChangeEvent, + handleChange: ( + accountId: number, + field: string, + value: string | number, + ) => void, + id: number, + field: string, +) => { + let input = event.target.value; + const pattern = /^-?\d*\.?\d{0,2}$/; + + console.log(pattern.test(input)); + + //Determine if the input follows the format/pattern + if (pattern.test(input)) { + input = input.replace(/^0+(?=\d)/, ""); + handleChange(id, field, input); + } +}; +export const handleListCurrencyBlur = ( + event: React.ChangeEvent, + handleChange: ( + accountId: number, + field: string, + value: string | number, + ) => void, + id: number, + field: string, +) => { + if (event.target.value !== "") { + handleChange(id, field, parseFloat(event.target.value).toFixed(2)); + } +}; diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 31b564a..0dca070 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -52,3 +52,17 @@ services: - ./default_container.env environment: - JWT_SECRET + + load-test: + image: grafana/k6:0.49.0 + # Mount the k6 scripts read-only so the same load test can be run + # either through Docker or locally with a host-installed k6 binary. + volumes: + - ./load-tests:/scripts:ro + environment: + - LOAD_TEST_BASE_URL=http://api-gateway:8000 + - LOAD_TEST_USERS=20 + - REQUESTS_PER_USER_PER_MINUTE=10 + - LOAD_TEST_DURATION=2m + command: run /scripts/api-gateway-capacity.js + diff --git a/app/server/load-tests/README.md b/app/server/load-tests/README.md new file mode 100644 index 0000000..7cb5ef7 --- /dev/null +++ b/app/server/load-tests/README.md @@ -0,0 +1,25 @@ +# Load Testing + +This folder contains the `k6` capacity test for the backend. + +The main script is `api-gateway-capacity.js`. + +It validates the course capacity requirement by simulating: + +- `20` concurrent users +- `200` total requests per minute + +How it works: + +- Each virtual user signs up and logs in with a unique test account. +- Each virtual user creates one account and one starter transaction. +- After setup, each virtual user sends `10` authenticated requests per minute. +- `20` users x `10` requests per minute = `200` requests per minute total. + +How to run it: + +```bash +cd app/server +docker compose up -d +docker compose -f docker-compose.test.yml up load-test --abort-on-container-failure +``` diff --git a/app/server/load-tests/api-gateway-capacity.js b/app/server/load-tests/api-gateway-capacity.js new file mode 100644 index 0000000..c0aedc2 --- /dev/null +++ b/app/server/load-tests/api-gateway-capacity.js @@ -0,0 +1,221 @@ +/* + Load testing script + + It is designed to be run either: + 1. through Docker using the command from app/server: + docker compose up -d + docker compose -f docker-compose.yml -f docker-compose.test.yml up load-test --abort-on-container-failure. + 2. Using k6 which needs to be installed first then run with from app/server/load-tests: + k6.exe run api-gateway-capacity.js + + Performance targets: + 20 concurrent users, each sending 10 requests per minute. + 20 users * 10 requests/minute = 200 requests/minute total. + + */ + +import http from "k6/http"; +import { check, sleep } from "k6"; +import { Counter } from "k6/metrics"; + +const CONCURRENT_USERS = Number(__ENV.LOAD_TEST_USERS || 20); +const REQUESTS_PER_USER_PER_MINUTE = Number( + __ENV.REQUESTS_PER_USER_PER_MINUTE || 10, +); +const TEST_DURATION = __ENV.LOAD_TEST_DURATION || "2m"; +const API_BASE_URL = __ENV.LOAD_TEST_BASE_URL || "http://localhost:3000"; +const EMAIL_RUN_ID = __ENV.LOAD_TEST_RUN_ID || Date.now().toString(); + +// Each iteration sends exactly one authenticated request, so sleeping for +// 60 / 10 = 6 seconds keeps one user at 10 requests per minute. +const REQUEST_INTERVAL_SECONDS = 60 / REQUESTS_PER_USER_PER_MINUTE; + +// Count successful business requests so the summary clearly shows whether +// the server handled the intended request volume successfully. +const successfulRequests = new Counter("successful_requests"); + +export const options = { + scenarios: { + capacity_requirement: { + executor: "constant-vus", + vus: CONCURRENT_USERS, + duration: TEST_DURATION, + }, + }, + thresholds: { + // The capacity test should be stable; failures indicate the system + // cannot reliably sustain the configured user/request volume. + http_req_failed: ["rate<0.01"], + checks: ["rate>0.99"], + }, + summaryTrendStats: ["avg", "min", "med", "p(90)", "p(95)", "max"], +}; + +let authToken; +let accountId; +let transactionSequence = 0; + +function buildUserIdentity(userNumber) { + const suffix = `${EMAIL_RUN_ID}_${userNumber}`; + return { + username: `load_user_${suffix}`, + email: `load_user_${suffix}@finus.test`, + password: "LoadTestPass123!", + first_name: "Load", + last_name: `User${userNumber}`, + age: 30, + }; +} + +function signupAndLoginForUser(userNumber) { + const user = buildUserIdentity(userNumber); + + // Signup provisions a real user/profile pair in the database so each + // user exercises the same auth and persistence flow as production. + const signupResponse = http.post( + `${API_BASE_URL}/api/signup`, + JSON.stringify(user), + { + headers: { "Content-Type": "application/json" }, + tags: { endpoint: "signup" }, + }, + ); + + // Re-runs should still work if the same run id is reused and the account + // already exists, so we accept the duplicate-account response as setup-safe. + check(signupResponse, { + "signup succeeded or already existed": (response) => + response.status === 201 || response.status === 409, + }); + + const loginResponse = http.post( + `${API_BASE_URL}/api/login`, + JSON.stringify({ + email: user.email, + password: user.password, + }), + { + headers: { "Content-Type": "application/json" }, + tags: { endpoint: "login" }, + }, + ); + + check(loginResponse, { + "login returned token": (response) => + response.status === 200 && Boolean(response.json("token")), + }); + + authToken = loginResponse.json("token"); +} + +function createAccountForUser(userNumber) { + // Each user creates one account once so read endpoints have + // meaningful user-owned data to query during the steady-state load. + const accountResponse = http.post( + `${API_BASE_URL}/api/accounts`, + JSON.stringify({ + id: 0, + name: `Load Checking ${EMAIL_RUN_ID}_${userNumber}`, + type: "CHEQUING", + balance: 1000, + value: 1000, + last_updated: new Date().toISOString(), + }), + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + tags: { endpoint: "create_account" }, + }, + ); + + check(accountResponse, { + "account created": (response) => + response.status === 200 || response.status === 201, + }); + + accountId = accountResponse.json("id"); +} + +function createTransactionForUser(userNumber) { + transactionSequence += 1; + + const transactionResponse = http.post( + `${API_BASE_URL}/api/transactions`, + JSON.stringify({ + id: 0, + financialAccount_id: accountId, + amount: 25 + transactionSequence, + description: `Load test transaction ${transactionSequence}`, + sender: `Employer_${userNumber}`, + recipient: `Load_User_${userNumber}`, + date: new Date().toISOString(), + category: "INCOME", + }), + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + tags: { endpoint: "create_transaction" }, + }, + ); + + const requestPassed = check(transactionResponse, { + "transaction created": (response) => + response.status === 200 || response.status === 201, + }); + + if (requestPassed) { + successfulRequests.add(1); + } +} + +function ensureUserIsInitialized() { + if (authToken && accountId) { + return; + } + + const userNumber = __VU; + signupAndLoginForUser(userNumber); + createAccountForUser(userNumber); + createTransactionForUser(userNumber); +} + +function authenticatedGet(path, endpointTag) { + const response = http.get(`${API_BASE_URL}${path}`, { + headers: { Authorization: `Bearer ${authToken}` }, + tags: { endpoint: endpointTag }, + }); + + const requestPassed = check(response, { + [`${endpointTag} returned 200`]: (res) => res.status === 200, + }); + + if (requestPassed) { + successfulRequests.add(1); + } + + return response; +} + +export default function () { + ensureUserIsInitialized(); + + // Rotate endpoints so the load is spread across the gateway and the user + // service instead of repeatedly hitting a single route. + const routeIndex = __ITER % 3; + if (routeIndex === 0) { + authenticatedGet("/api/accounts", "accounts_list"); + } else if (routeIndex === 1) { + authenticatedGet( + `/api/transactions?financialAccount_id=${accountId}`, + "transaction_list", + ); + } else { + createTransactionForUser(__VU); + } + + sleep(REQUEST_INTERVAL_SECONDS); +} diff --git a/app/server/services/python/analytics/src/logic/savings.py b/app/server/services/python/analytics/src/logic/savings.py index 43166dd..a822b14 100644 --- a/app/server/services/python/analytics/src/logic/savings.py +++ b/app/server/services/python/analytics/src/logic/savings.py @@ -1,13 +1,8 @@ import pandas as pd -import numpy as np from typing import List, Dict -from datetime import datetime, date +from datetime import date from dateutil.relativedelta import relativedelta -from fastapi import HTTPException -# from ..queries import savings as savings_queries -from src.dependencies import get_db_connection -from src.models.schemas import ProjectedSavingsRequest, MonthlySavingGrowthRate, CompoundInterestResponse -from src.queries.savings import get_savings_transactions +from src.models.schemas import ProjectedSavingsRequest, CompoundInterestResponse def calculate_savings_over_time( accounts: List[Dict], @@ -58,152 +53,53 @@ def calculate_savings_over_time( }] } -# def get_monthly_balances(transactions: pd.DataFrame): -# df = transactions.copy() -# df['date'] = pd.to_datetime(df['date']) - -# # Sort by date -# df = df.sort_values('date') - -# # Group by month -# df['year_month'] = df['date'].dt.to_period('M') - -# monthly_sums = df.groupby('year_month')['amount'].sum().reset_index() - -# monthly_sums['year_month'] = monthly_sums['year_month'].dt.to_timestamp() - -# # Create full monthly range -# full_range = pd.date_range( -# start=monthly_sums['year_month'].min(), -# end=monthly_sums['year_month'].max(), -# freq='MS' # month start -# ) - -# monthly_sums = monthly_sums.set_index('year_month').reindex(full_range, fill_value=0) -# monthly_sums = monthly_sums.rename_axis('date').reset_index() - -# # Convert to cumulative balance -# monthly_sums['balance'] = monthly_sums['amount'].cumsum() -# print(monthly_sums) - -# return monthly_sums - -def get_monthly_balances(transactions: pd.DataFrame): - df = transactions.copy() - df['date'] = pd.to_datetime(df['date']) - - # Extract month only (1–12) - df['month'] = df['date'].dt.month - - # Initialize 12 months with no balance - monthly_totals = {month: 0 for month in range(1, 13)} - - # Aggregate ignoring year - for _, row in df.iterrows(): - monthly_totals[row['month']] += row['amount'] - - # Convert to DataFrame (ordered) - monthly_df = pd.DataFrame([ - {"month": m, "amount": monthly_totals[m]} - for m in range(1, 13) - ]) - - # Compute cumulative balance for each month - monthly_df['balance'] = monthly_df['amount'].cumsum() - - print(monthly_df) - - return monthly_df - -def compute_monthly_growth_rates(monthly_df: pd.DataFrame): - # Compute increase rate between months - monthly_df['growth_rate'] = monthly_df['balance'].pct_change() - - # Replace inf and -inf with NaN and drop NaN - monthly_df['growth_rate'].replace([np.inf, -np.inf], np.nan, inplace=True) - - # Replace NaN with 0 - monthly_df['growth_rate'].fillna(0, inplace=True) - - growth_rates = monthly_df['growth_rate'] - growth_rates = growth_rates.round(3) - - return growth_rates - -def generate_savings_growth_rate(transactions: List[Dict]) -> MonthlySavingGrowthRate: - df = pd.DataFrame(transactions) - - if df.empty: - return None - - monthly_balances = get_monthly_balances(df) - growth_rates = compute_monthly_growth_rates(monthly_balances) - - mean_growth = growth_rates.mean() - standard_deviation_growth = growth_rates.std() - - return MonthlySavingGrowthRate( - best_case=round(mean_growth + standard_deviation_growth, 2), - expected_case=round(mean_growth, 2), - worst_case=max(round(mean_growth - standard_deviation_growth, 2), 0) - ) - def calculate_compound_interest(request: ProjectedSavingsRequest) -> List[CompoundInterestResponse]: - connection = get_db_connection() - if not connection: - raise HTTPException(status_code=500, detail="Database connection failed") - - cursor = connection.cursor(dictionary=True) - transactions = get_savings_transactions(cursor, [request.financial_account_id]) - print(transactions) - if len(transactions) == 0: - return [] - - monthly_savings_rate = generate_savings_growth_rate(transactions) - print(monthly_savings_rate) - - best_rate = monthly_savings_rate.best_case - worst_rate = monthly_savings_rate.worst_case - expected_rate = monthly_savings_rate.expected_case + annual_rate = request.annual_interest_rate / 100 + monthly_rate = annual_rate / 12 + worst_balance = request.balance expected_balance = request.balance best_balance = request.balance - + monthly_deposit = request.monthly_deposit time_frame = request.time_frame - + results: List[CompoundInterestResponse] = [] - + months = time_frame * 12 current_date = date.today() - for _ in range(months): - # deposit money + # Monte Carlo simulation approach is more robust + best_monthly_rate = monthly_rate * 2 + worst_monthly_rate = monthly_rate * 0.5 + + for month in range(months): + # add monthly deposit at beginning of month worst_balance += monthly_deposit expected_balance += monthly_deposit best_balance += monthly_deposit - - # compute growth - worst_balance += worst_balance * worst_rate - expected_balance += expected_balance * expected_rate - best_balance += best_balance * best_rate - - # rounding + + # apply compound interest (standard formula) + worst_balance = worst_balance * (1 + worst_monthly_rate) + expected_balance = expected_balance * (1 + monthly_rate) + best_balance = best_balance * (1 + best_monthly_rate) + worst_balance = round(worst_balance, 2) expected_balance = round(expected_balance, 2) best_balance = round(best_balance, 2) - + + # calculate next month's date + next_date = current_date + relativedelta(months=month + 1) + results.append( CompoundInterestResponse( accumulative_worst_balance=worst_balance, accumulative_expected_balance=expected_balance, accumulative_best_balance=best_balance, - date=current_date.strftime("%B %Y") + date=next_date.strftime("%B %Y") ) ) - - current_date += relativedelta(months=1) - + return results \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_savings.py b/app/server/services/python/analytics/test/test_savings.py index 53e4ed2..c214666 100644 --- a/app/server/services/python/analytics/test/test_savings.py +++ b/app/server/services/python/analytics/test/test_savings.py @@ -1,11 +1,12 @@ -from src.models.schemas import ProjectedSavingsRequest -import pytest +# tests/test_savings.py import pandas as pd -from datetime import datetime, timedelta -from unittest.mock import patch, MagicMock -from src.logic.savings import calculate_savings_over_time, calculate_compound_interest, generate_savings_growth_rate, compute_monthly_growth_rates, get_monthly_balances +import pytest +from datetime import date, timedelta +from dateutil.relativedelta import relativedelta +from src.models.schemas import ProjectedSavingsRequest, CompoundInterestResponse +from src.logic.savings import calculate_compound_interest, calculate_savings_over_time -class TestSavings: +class TestSavingsProjection: @pytest.fixture def mock_savings_transactions(self): @@ -29,6 +30,27 @@ def mock_savings_accounts(self): {'id': 1, 'balance': 5000}, {'id': 2, 'balance': 3000}, ] + + @pytest.fixture + def mock_savings_request(self): + return ProjectedSavingsRequest( + financial_account_id=1, + balance=1000.00, + monthly_deposit=100.00, + annual_interest_rate=5.0, # 5% annual interest + time_frame=1 # 1 year + ) + + @pytest.fixture + def expected_compound_calculation(self): + """Helper to calculate expected values for verification""" + def calculate(balance, monthly_deposit, monthly_rate, months): + result = balance + for _ in range(months): + result += monthly_deposit + result *= (1 + monthly_rate) + return round(result, 2) + return calculate def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_savings_transactions): end_date = pd.Timestamp('2024-03-15') @@ -121,35 +143,169 @@ def test_calculate_savings_over_time_no_accounts(self): assert result['labels'] == [] assert result['datasets'][0]['data'] == [] - def test_get_monthly_balances(self, mock_savings_transactions_by_financial_account): + def test_calculate_compound_interest_basic(self, mock_savings_request, expected_compound_calculation): + result = calculate_compound_interest(mock_savings_request) + + # Verify result structure + assert isinstance(result, list) + assert len(result) == 12 # 1 year = 12 months + + # Verify first month + first_month = result[0] + assert isinstance(first_month, CompoundInterestResponse) + assert first_month.accumulative_expected_balance > mock_savings_request.balance + + # Verify monthly progression + for i in range(1, len(result)): + assert result[i].accumulative_expected_balance > result[i-1].accumulative_expected_balance + + # Verify date formatting + current_date = date.today() + first_expected_date = (current_date + relativedelta(months=1)).strftime("%B %Y") + assert result[0].date == first_expected_date + + def test_calculate_compound_interest_zero_balance(self, mock_savings_request): + mock_savings_request.balance = 0.0 + result = calculate_compound_interest(mock_savings_request) + + assert len(result) == 12 + # After 12 months of $100 deposits with interest, should be > $1200 + assert result[-1].accumulative_expected_balance > 1200.0 + + def test_calculate_compound_interest_zero_deposit(self, mock_savings_request): + mock_savings_request.monthly_deposit = 0.0 + result = calculate_compound_interest(mock_savings_request) + + # Should still grow from interest alone + assert result[-1].accumulative_expected_balance > mock_savings_request.balance - result = get_monthly_balances(mock_savings_transactions_by_financial_account) + def test_calculate_compound_interest_high_interest_rate(self, mock_savings_request): + mock_savings_request.annual_interest_rate = 20.0 # 20% annual + result = calculate_compound_interest(mock_savings_request) + + # Growth should be significantly higher + assert result[-1].accumulative_expected_balance > 2000.0 - # January = 150, February = 200 - assert result.loc[result['month'] == 1, 'amount'].values[0] == 150 - assert result.loc[result['month'] == 2, 'amount'].values[0] == 200 + def test_calculate_compound_interest_zero_interest_rate(self, mock_savings_request): + mock_savings_request.annual_interest_rate = 0.0 + result = calculate_compound_interest(mock_savings_request) + + # With no interest, final balance should be: balance + (monthly_deposit * 12) + expected = mock_savings_request.balance + (mock_savings_request.monthly_deposit * 12) + assert result[-1].accumulative_expected_balance == expected - # cumulative balance - assert result.loc[result['month'] == 1, 'balance'].values[0] == 150 - assert result.loc[result['month'] == 2, 'balance'].values[0] == 350 + def test_calculate_compound_interest_negative_interest_rate(self, mock_savings_request): + mock_savings_request.annual_interest_rate = -5.0 + result = calculate_compound_interest(mock_savings_request) + + # With negative interest, balance should decrease + expected_no_interest = mock_savings_request.balance + (mock_savings_request.monthly_deposit * 12) + assert result[-1].accumulative_expected_balance < expected_no_interest - def test_compute_growth_rates_handles_nan_and_inf(self, mock_monthly_diff): + def test_calculate_compound_interest_long_term(self, mock_savings_request): + mock_savings_request.time_frame = 5 + result = calculate_compound_interest(mock_savings_request) + + assert len(result) == 60 # 5 years * 12 months + + # Verify best/west/expected ordering + for month in result: + assert month.accumulative_best_balance >= month.accumulative_expected_balance >= month.accumulative_worst_balance - growth_rates = compute_monthly_growth_rates(mock_monthly_diff) + def test_calculate_compound_interest_short_term(self, mock_savings_request): + mock_savings_request.time_frame = 1 # 1 year + result = calculate_compound_interest(mock_savings_request) + + assert len(result) == 12 + assert result[0].accumulative_expected_balance > mock_savings_request.balance - # Should not contain NaN or inf - assert not growth_rates.isnull().any() - assert not (growth_rates == float("inf")).any() + def test_calculate_compound_interest_rounding(self, mock_savings_request): + result = calculate_compound_interest(mock_savings_request) + + for month in result: + # Check that values have at most 2 decimal places + best_str = f"{month.accumulative_best_balance:.10f}" + expected_str = f"{month.accumulative_expected_balance:.10f}" + worst_str = f"{month.accumulative_worst_balance:.10f}" + + # After the decimal point, there should be at most 2 non-zero digits + # or they should be exactly 0 + best_decimal = best_str.split('.')[1].rstrip('0') + expected_decimal = expected_str.split('.')[1].rstrip('0') + worst_decimal = worst_str.split('.')[1].rstrip('0') + + assert len(best_decimal) <= 2 or best_decimal == '' + assert len(expected_decimal) <= 2 or expected_decimal == '' + assert len(worst_decimal) <= 2 or worst_decimal == '' - def test_generate_growth_rate(self, mock_transactions_with_same_amount): + def test_calculate_compound_interest_best_worst_cases(self, mock_savings_request): + result = calculate_compound_interest(mock_savings_request) + + for month in result: + assert month.accumulative_best_balance >= month.accumulative_expected_balance + assert month.accumulative_expected_balance >= month.accumulative_worst_balance - result = generate_savings_growth_rate(mock_transactions_with_same_amount) + def test_calculate_compound_interest_monthly_progression(self, mock_savings_request): + result = calculate_compound_interest(mock_savings_request) + + for i in range(1, len(result)): + # Expected case should always increase + assert result[i].accumulative_expected_balance > result[i-1].accumulative_expected_balance + + # Best case should always increase + assert result[i].accumulative_best_balance > result[i-1].accumulative_best_balance + + # Worst case should always increase (unless negative interest) + assert result[i].accumulative_worst_balance > result[i-1].accumulative_worst_balance - assert result is not None - assert result.best_case >= result.expected_case - assert result.expected_case >= result.worst_case + def test_calculate_compound_interest_large_values(self, mock_savings_request): + mock_savings_request.balance = 1000000.00 + mock_savings_request.monthly_deposit = 50000.00 + mock_savings_request.time_frame = 10 + + result = calculate_compound_interest(mock_savings_request) + + # Should still produce valid numbers (not inf or NaN) + assert len(result) == 120 + assert all(isinstance(m.accumulative_expected_balance, float) for m in result) + assert all(m.accumulative_expected_balance < float('inf') for m in result) + assert all(not pd.isna(m.accumulative_expected_balance) for m in result) - def test_generate_growth_rate_empty(self): - result = generate_savings_growth_rate([]) + def test_calculate_compound_interest_different_accounts(self): + test_cases = [ + (5000, 200, 3.5, 2), # High balance, moderate deposits + (100, 500, 7.0, 3), # Low balance, high deposits + (10000, 0, 4.0, 1), # High balance, no deposits + (0, 1000, 6.0, 5), # Zero balance, high deposits + ] + + for balance, deposit, rate, years in test_cases: + request = ProjectedSavingsRequest( + financial_account_id=1, + balance=balance, + monthly_deposit=deposit, + annual_interest_rate=rate, + time_frame=years + ) + result = calculate_compound_interest(request) + + assert len(result) == years * 12 + # Final balance should be greater than starting balance + total deposits + total_deposits = deposit * (years * 12) + if rate > 0: + assert result[-1].accumulative_expected_balance > balance + total_deposits + else: + assert result[-1].accumulative_expected_balance == balance + total_deposits - assert result is None \ No newline at end of file + def test_calculate_compound_interest_date_handling(self, mock_savings_request): + result = calculate_compound_interest(mock_savings_request) + + # Parse dates and verify they are sequential + from datetime import datetime + dates = [datetime.strptime(r.date, "%B %Y") for r in result] + + for i in range(1, len(dates)): + # Each month should be exactly 1 month apart + expected_next = dates[i-1] + relativedelta(months=1) + assert dates[i].year == expected_next.year + assert dates[i].month == expected_next.month \ No newline at end of file diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts index 5ba37c1..092c564 100644 --- a/app/server/services/ts/api-gateway/src/index.ts +++ b/app/server/services/ts/api-gateway/src/index.ts @@ -36,9 +36,11 @@ registerProxy(process.env.USER_SERVICE_ADDR, [ "/profiles", "/goals", "/debts", + "/savings", "/transactions", "/charts/expenses", "/table/transactions", + "/table/transactions/accounts", "/table/snapshot", "/transactions/csvTransaction", ]); diff --git a/app/server/services/ts/user/Dockerfile.integration.test b/app/server/services/ts/user/Dockerfile.integration.test index 51af2d2..7a68957 100644 --- a/app/server/services/ts/user/Dockerfile.integration.test +++ b/app/server/services/ts/user/Dockerfile.integration.test @@ -8,3 +8,5 @@ COPY vitest.config.ts . ENV CI=true CMD ["bun", "test", "integration", "--coverage"] + + diff --git a/app/server/services/ts/user/Dockerfile.userInp.test b/app/server/services/ts/user/Dockerfile.userInp.test new file mode 100644 index 0000000..7ea27e2 --- /dev/null +++ b/app/server/services/ts/user/Dockerfile.userInp.test @@ -0,0 +1,11 @@ +FROM measureonecodetwice/finus-user:local + +COPY user/userInput ./test + +WORKDIR /project + +COPY vitest.config.ts . + +ENV CI=true + +CMD [ "bun", "run", "vitest", "--run"] diff --git a/app/server/services/ts/user/integration/accounts.test.ts b/app/server/services/ts/user/integration/accounts.test.ts index 00743ea..53b02b6 100644 --- a/app/server/services/ts/user/integration/accounts.test.ts +++ b/app/server/services/ts/user/integration/accounts.test.ts @@ -81,9 +81,8 @@ describe("Accounts Integration (Docker)", () => { it("deletes an account", async () => { const res = await request(BASE_URL) - .delete("/api/accounts") - .set("Authorization", `Bearer ${token}`) - .send({ id: accountId }); + .delete(`/api/accounts?id=${accountId}`) + .set("Authorization", `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.message).toBe("Account successfully deleted"); diff --git a/app/server/services/ts/user/src/CheckUser.ts b/app/server/services/ts/user/src/CheckUser.ts index 3a92f34..e7d8bc7 100644 --- a/app/server/services/ts/user/src/CheckUser.ts +++ b/app/server/services/ts/user/src/CheckUser.ts @@ -1,39 +1,67 @@ import type { RowDataPacket, Connection } from "mysql2/promise"; //Checks the user is the owner of the account +// export async function checkUserId( +// db: Connection, +// userId: number, +// accountId: number, +// ): Promise { +// let result = false; +// try { +// //need the user account's profile id first. This should be just a single item returned unless stretch feature 7 is implemented +// // const [profileRows] = await db.query( +// // `SELECT profile_id FROM finusAccount_profile WHERE account_id = ?`, +// // [userId], +// // ); + +// const [profileRows] = await db.query( +// `SELECT uid from` + +// if (!profileRows || (profileRows as RowDataPacket[]).length === 0) { +// throw new Error("User profile not found"); +// } + +// const profileId = (profileRows as RowDataPacket[])[0].profile_id; + +// //Checks if the profile has an account with that id +// const [rows] = await db.query( +// `SELECT 1 FROM profile_financialAccount +// WHERE profile_id =? AND financialAccount_id =?`, +// [profileId, accountId], +// ); + +// console.log(rows.length); +// if (rows.length > 0) { +// result = true; +// } +// } catch (error) { +// console.error(error); +// } + +// return result; +// } + export async function checkUserId( db: Connection, userId: number, accountId: number, ): Promise { - let result = false; try { - //need the user account's profile id first. This should be just a single item returned unless stretch feature 7 is implemented - const [profileRows] = await db.query( - `SELECT profile_id FROM finusAccount_profile WHERE account_id = ?`, - [userId], - ); - - if (!profileRows || (profileRows as RowDataPacket[]).length === 0) { - throw new Error("User profile not found"); - } - - const profileId = (profileRows as RowDataPacket[])[0].profile_id; - - //Checks if the profile has an account with that id + //single query joining through the relationship chain const [rows] = await db.query( - `SELECT 1 FROM profile_financialAccount - WHERE profile_id =? AND financialAccount_id =?`, - [profileId, accountId], + `SELECT 1 + FROM profile_financialAccount pfa + INNER JOIN finusAccount_profile uap ON pfa.profile_id = uap.profile_id + WHERE uap.account_id = ? AND pfa.financialAccount_id = ?`, + [userId, accountId], ); - - console.log(rows.length); - if (rows.length > 0) { - result = true; - } + console.log( + `checking account id:${accountId} for user ${userId}, rows found: `, + rows.length, + ); + return rows.length > 0; } catch (error) { - console.error(error); + console.error("Error in checkUserId:", error); + return false; } - - return result; } diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts index f968850..aabe250 100644 --- a/app/server/services/ts/user/src/index.ts +++ b/app/server/services/ts/user/src/index.ts @@ -4,7 +4,10 @@ import { authenticateJWT } from "./utils/auth.ts"; import { generateDateRange } from "./utils/dates.ts"; import { validatePeriod } from "./validation.ts"; import { getExpensesChartData } from "./logic/expenses.ts"; -import { getTransactionsData } from "./logic/transactions.ts"; +import { + getAccountIDsForUser, + getTransactionsData, +} from "./logic/transactions.ts"; import { getSnapshotData } from "./logic/snapshot.ts"; import { onExit } from "@/hooks"; import { buildCorsConfig } from "@/expressUtils"; @@ -16,6 +19,7 @@ import { getUserGoals, getUserGoalById, } from "./logic/goals.ts"; +import { getUserProfileId } from "./queries/goals.ts"; import type { GoalType, UpdateGoalInput } from "./types/Goals.ts"; import { getGoalCountByProfileId } from "./queries/goals.ts"; import { accountsRouter } from "./routes/account.js"; @@ -23,6 +27,12 @@ import { profilesRouter } from "./routes/profile.js"; import { transactionsRouter } from "./routes/transaction.js"; import { debtRouter } from "./routes/debt.ts"; import { savingRouter } from "./routes/saving.ts"; +import type { Transaction } from "./types/Transaction.ts"; +import { + createTransaction, + deleteTransactionQuery, + updateTransactionQuery, +} from "./queries/transactions.ts"; const pool = getConnectionPool(); const app = express(); @@ -32,7 +42,6 @@ app.use(express.json()); app.use((req, res, next) => { console.log("USER Incoming request: " + req.method + " " + req.url); - console.log(req.body); next(); }); @@ -88,6 +97,7 @@ app.get( return res.json([]); // Return empty array for no transactions } + // console.log("Fetched transactions for user:", transactions); res.json(transactions); } catch (error) { console.error("Error fetching transactions:", error); @@ -100,6 +110,220 @@ app.get( }, ); +app.get( + "/table/transactions/accounts", + async (req: express.Request, res: express.Response) => { + try { + const userId = authenticateJWT(req); + const accountsMap = await getAccountIDsForUser(pool, userId); + + //convert Map to array of objects + const accountsArray: { id: number; name: string }[] = []; + if (accountsMap instanceof Map) { + accountsMap.forEach((name, id) => { + accountsArray.push({ id: Number(id), name }); + }); + } + + res.json(accountsArray); + } catch (error) { + console.error("Error fetching transactions:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ error: "Failed to fetch transactions" }); + } + } + }, +); + +//create a new transaction through the table +app.post( + "/table/transactions", + async (req: express.Request, res: express.Response) => { + try { + const userId = authenticateJWT(req); + if (!userId) { + return res.status(401).json({ error: "User not authenticated" }); + } + //extract financial account id as query param + const financialAccountId = req.query.fid as string; + if (!financialAccountId) { + return res.status(400).json({ error: "Missing financial account ID" }); + } + + //validate request body + if (!req.body) { + return res.status(400).json({ error: "Invalid request body" }); + } + + let description = null; + let category = null; + let amount = null; + let date = null; + let sender = null; + let recipient = null; + + if (req.body.description) description = req.body.description; + if (req.body.category) category = req.body.category; + if (req.body.amount) amount = req.body.amount; + if (req.body.date) date = req.body.date; + if (req.body.sender) sender = req.body.sender; + if (req.body.recipient) recipient = req.body.recipient; + + if ( + !description || + !category || + !amount || + !date || + !sender || + !recipient + ) { + return res.status(400).json({ + error: "Missing required fields for creating a transaction", + }); + } + + const transactionData: Transaction = { + id: 0, //this is ignored in creation because it is auto generated + financialAccount_id: financialAccountId as unknown as number, + amount: amount, + category: category, + description: description, + sender: sender, + recipient: recipient, + date: date, + constructor: { + name: "RowDataPacket", + }, + }; + + // console.log("Creating transaction with data:", transactionData); + + const transaction = await createTransaction( + pool, + financialAccountId as unknown as number, + transactionData, + ); + + res.json(transaction); + } catch (error) { + console.error("Error creating transaction:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ error: "Failed to create transaction" }); + } + } + }, +); + +//delete a transaction through the table +app.delete( + "/table/transactions", + async (req: express.Request, res: express.Response) => { + try { + const userId = authenticateJWT(req); + if (!userId) { + return res.status(401).json({ error: "User not authenticated" }); + } + + //extract transaction id as query param + const transactionId = req.query.tid as unknown as number; + if (!transactionId) { + return res.status(400).json({ error: "Missing transaction ID" }); + } + + //get user profile id + const userProfileId = await getUserProfileId(pool, userId); + if (!userProfileId) { + return res.status(404).json({ error: "User profile not found" }); + } + + //delete the transaciton + const deleted = await deleteTransactionQuery( + pool, + transactionId, + userProfileId, + ); + if (!deleted) { + return res.status(404).json({ error: "Transaction not found" }); + } + return res.status(200).send(); + } catch (error) { + console.error("Error deleting transaction:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ error: "Failed to delete transaction" }); + } + } + }, +); + +//update a transaction through the table +app.patch( + "/table/transactions", + async (req: express.Request, res: express.Response) => { + try { + const userId = authenticateJWT(req); + if (!userId) { + return res.status(401).json({ error: "User not authenticated" }); + } + + //extract transaction id as query param + const transactionId = req.query.tid as unknown as number; + if (!transactionId) { + return res.status(400).json({ error: "Missing transaction ID" }); + } + + //validate request body + if (!req.body) { + return res.status(400).json({ error: "Invalid request body" }); + } + + //need profile id to verify ownership of the transaction via account + const userProfileId = await getUserProfileId(pool, userId); + if (!userProfileId) { + return res.status(404).json({ error: "User profile not found" }); + } + + const updates: Partial = {}; + if (req.body.description !== undefined) + updates.description = req.body.description; + if (req.body.category !== undefined) updates.category = req.body.category; + if (req.body.amount !== undefined) updates.amount = req.body.amount; + if (req.body.date !== undefined) updates.date = req.body.date; + if (req.body.sender !== undefined) updates.sender = req.body.sender; + if (req.body.recipient !== undefined) + updates.recipient = req.body.recipient; + if (req.body.financialAccount_id !== undefined) + updates.financialAccount_id = req.body.financialAccount_id; + + // console.log("Updating transaction with the following updates:", updates); + + const transaction = await updateTransactionQuery( + pool, + transactionId, + userProfileId, + updates, + ); + if (!transaction) { + return res.status(404).json({ error: "Transaction not found" }); + } + + res.json(transaction); + } catch (error) { + console.error("Error updating transaction:", error); + if (error instanceof Error && error.message.includes("Authorization")) { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ error: "Failed to update transaction" }); + } + } + }, +); + app.get( "/table/snapshot", async (req: express.Request, res: express.Response) => { @@ -210,7 +434,7 @@ app.patch("/goals", async (req: express.Request, res: express.Response) => { // const goalId = parseInt(req.query.gid); let goalId = null; - if (req.query.gid) goalId = parseInt(req.query.gid); + if (req.query.gid) goalId = req.query.gid as unknown as number; //parseInt() if (!goalId) { return res.status(404).json({ error: "Missing goal ID" }); @@ -276,7 +500,7 @@ app.delete("/goals", async (req: express.Request, res: express.Response) => { let goalId = null; - if (req.query.gid) goalId = parseInt(req.query.gid); + if (req.query.gid) goalId = req.query.gid as unknown as number; //parseInt() if (!goalId) { return res.status(404).json({ error: "Missing goal ID" }); diff --git a/app/server/services/ts/user/src/logic/goals.ts b/app/server/services/ts/user/src/logic/goals.ts index fac6085..90ce360 100644 --- a/app/server/services/ts/user/src/logic/goals.ts +++ b/app/server/services/ts/user/src/logic/goals.ts @@ -99,14 +99,14 @@ export async function enrichGoalWithProgress( 100, ); - console.log( - "enriching goal with progress, cur amount:", - current_amount, - "progress percentage:", - progress_percentage, - " because target is:", - goal.target, - ); + // console.log( + // "enriching goal with progress, cur amount:", + // current_amount, + // "progress percentage:", + // progress_percentage, + // " because target is:", + // goal.target, + // ); return { ...goal, diff --git a/app/server/services/ts/user/src/logic/transactions.ts b/app/server/services/ts/user/src/logic/transactions.ts index bb1e4c5..fe286ff 100644 --- a/app/server/services/ts/user/src/logic/transactions.ts +++ b/app/server/services/ts/user/src/logic/transactions.ts @@ -1,14 +1,37 @@ -import type { Pool } from "mysql2/promise"; -import { getAllTransactionsQuery } from "../queries/transactions.ts"; +import type { Pool, RowDataPacket } from "mysql2/promise"; +import { + getAllTransactionsQuery, + getTransactionAccountNames, +} from "../queries/transactions.ts"; import type { Transaction } from "../types/Transaction.ts"; +//returns a key value map of account id to account name +export async function getAccountIDsForUser( + pool: Pool, + userId: string, +): Promise> { + const query = ` + SELECT fa.id, fa.name + FROM finus.financialAccount fa + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + JOIN finus.finusAccount_profile uap ON pfa.profile_id = uap.profile_id + WHERE uap.account_id = ? + `; + const [rows] = await pool.query(query, [userId]); + const output = new Map(); + for (const row of rows) { + output.set(row.id, row.name); + } + return output; +} + export async function getTransactionsData( pool: Pool, userId: string, ): Promise { const transactions = await getAllTransactionsQuery(pool, userId); - // Enrich transactions with sender/recipient names if missing + //enrich transactions with sender/recipient names if missing if (transactions.length > 0) { const first_name = transactions[0].first_name || ""; const last_name = transactions[0].last_name || ""; @@ -24,5 +47,24 @@ export async function getTransactionsData( }); } + //get a list of all the financial account ids in the transactions and fetch the names of those accounts + const financialAccountIds = Array.from( + new Set(transactions.map((t) => t.financialAccount_id)), + ); + const accountIdToNameMap: Record = {}; + if (financialAccountIds.length > 0) { + const accountNames = await getTransactionAccountNames( + pool, + financialAccountIds, + ); + Object.assign(accountIdToNameMap, accountNames); + } + + //enrich transactions with account names + transactions.forEach((transaction) => { + transaction.account_name = + accountIdToNameMap[transaction.financialAccount_id] || "Unknown Account"; + }); + return transactions; } diff --git a/app/server/services/ts/user/src/queries/transactions.ts b/app/server/services/ts/user/src/queries/transactions.ts index 88e8243..088c26c 100644 --- a/app/server/services/ts/user/src/queries/transactions.ts +++ b/app/server/services/ts/user/src/queries/transactions.ts @@ -1,5 +1,6 @@ -import type { Pool } from "mysql2/promise"; +import type { Pool, ResultSetHeader, RowDataPacket } from "mysql2/promise"; import type { Transaction } from "../types/Transaction.ts"; +interface TransactionRow extends RowDataPacket, Transaction {} export async function getAllTransactionsQuery( pool: Pool, @@ -21,6 +22,23 @@ export async function getAllTransactionsQuery( return rows; } +export async function getTransactionAccountNames( + pool: Pool, + financialAccountIds: number[], +): Promise> { + if (financialAccountIds.length === 0) { + return {}; + } + const query = `SELECT id, name FROM finus.financialAccount WHERE id IN (${financialAccountIds.join(", ")});`; + + const [rows] = await pool.query(query); + const accountIdToNameMap: Record = {}; + for (const row of rows as RowDataPacket[]) { + accountIdToNameMap[row.id] = row.name; + } + return accountIdToNameMap; +} + export async function findTransactionsBy( db: Pool, financialAccountId: string, @@ -127,3 +145,171 @@ export async function getProfileCategoryExpensesQuery( const [rows] = await pool.query(query, [profileId, category]); return rows; } + +//creates a single transaction +export async function createTransactionQuery( + pool: Pool, + financialAccountId: number, + amount: number, + category: string, + date: Date, + sender: string, + recipient: string, + description: string, +): Promise { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const [result] = await connection.query( + `INSERT INTO finus.transaction (financialAccount_id, amount, category, date, sender, recipient, description) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + financialAccountId, + amount, + category, + date, + sender, + recipient, + description, + ], + ); + + if ((result as ResultSetHeader).affectedRows === 0) { + await connection.rollback(); + return false; + } + + await connection.commit(); + return true; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} + +export async function deleteTransactionQuery( + pool: Pool, + transactionId: number, + profileId: number, +): Promise { + const query = ` + DELETE t + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + WHERE t.id = ? AND pfa.profile_id = ? + `; + + const [result] = await pool.query(query, [ + transactionId, + profileId, + ]); + // console.log("Delete transaction result:", result); + return result.affectedRows > 0; +} + +export async function getTransactionById( + pool: Pool, + transactionId: number, + profileId: number, +): Promise { + const query = ` + SELECT t.* + FROM finus.transaction t + JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id + JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id + WHERE t.id = ? AND pfa.profile_id = ? + `; + + const [rows] = await pool.query(query, [ + transactionId, + profileId, + ]); + return rows[0]; +} + +export async function updateTransactionQuery( + pool: Pool, + transactionId: number, + profileId: number, //for verification that the user owns this transaction via account + updates: Partial, +): Promise { + //verify the transaction belongs to the user's profile + const existing = await getTransactionById(pool, transactionId, profileId); + if (!existing) return undefined; + + //define allowed fields for transaction updates + const allowedFields = [ + "amount", + "category", + "description", + "sender", + "recipient", + "date", + "financialAccount_id", + ]; + + const setClauses: string[] = []; + const values: unknown[] = []; + + //build update query dynamically since some fields may not be provided + for (const field of allowedFields) { + if (updates[field as keyof Transaction] !== undefined) { + setClauses.push(`${field} = ?`); + values.push(updates[field as keyof Transaction]); + } + } + + //if no fields to update, return the existing transaction + if (setClauses.length === 0) { + return existing; + } + + values.push(transactionId); + + await pool.query( + `UPDATE finus.transaction SET ${setClauses.join(", ")} WHERE id = ?`, + values, + ); + + //fetch the updated transaction + const [updated] = await pool.query( + `SELECT * FROM finus.transaction WHERE id = ?`, + [transactionId], + ); + + return updated[0]; +} + +export async function createTransaction( + pool: Pool, + financialAccountId: number, + transactionData: Transaction, +): Promise { + const { amount, category, description, sender, recipient, date } = + transactionData; + + const [result] = await pool.query( + `INSERT INTO finus.transaction + (financialAccount_id, amount, category, description, sender, recipient, date) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + financialAccountId, + amount, + category, + description, + sender, + recipient, + date, + ], + ); + + const [newTransaction] = await pool.query( + `SELECT * FROM finus.transaction WHERE id = ?`, + [result.insertId], + ); + + return newTransaction[0]; +} diff --git a/app/server/services/ts/user/src/routes/account.ts b/app/server/services/ts/user/src/routes/account.ts index 3a2ed8e..cfbe250 100644 --- a/app/server/services/ts/user/src/routes/account.ts +++ b/app/server/services/ts/user/src/routes/account.ts @@ -9,7 +9,7 @@ import type { RowDataPacket, } from "mysql2/promise"; import type { financialAccount } from "@/types.js"; -import { authenticateJWT } from "../handleJWT.js"; +import { authenticateJWT } from "../utils/auth.ts"; //this used to be ../handleJWT.ts but that does not work right import { checkUserId } from "../CheckUser.ts"; import { pool } from "../db.ts"; @@ -125,7 +125,7 @@ accountsRouter.get("/", async (req: Request, res: Response) => { [profileId], ); - console.log(rows); + // console.log(rows); return res.status(200).json(rows); } catch (err) { @@ -173,6 +173,10 @@ accountsRouter.put("/", async (req: Request, res: Response) => { } const { id, name, type, balance, value, subtype } = req.body; + let valueUpdate = value; + if (!value) { + valueUpdate = balance; + } const last_updated = new Date(); @@ -180,10 +184,10 @@ accountsRouter.put("/", async (req: Request, res: Response) => { `UPDATE financialAccount SET name = ?, type = ?, balance = ?, value = ?, last_updated = ?, subtype = ? WHERE id= ?`, - [name, type, balance, value, last_updated, subtype ?? null, id], + [name, type, balance, valueUpdate, last_updated, subtype ?? null, id], ); - console.log("Updated Account " + name); + // console.log("Updated Account " + name); return res.status(200).json({ message: "Account successfully updated", lastUpdated: last_updated, @@ -201,7 +205,8 @@ accountsRouter.put("/", async (req: Request, res: Response) => { accountsRouter.delete("/", async (req: Request, res: Response) => { let userId; let connection: PoolConnection | undefined; - const { id } = req.body; + + const id = req.query.id ? Number(req.query.id) : null; try { userId = authenticateJWT(req); diff --git a/app/server/services/ts/user/src/routes/transaction.ts b/app/server/services/ts/user/src/routes/transaction.ts index 3318ee6..b8845d8 100644 --- a/app/server/services/ts/user/src/routes/transaction.ts +++ b/app/server/services/ts/user/src/routes/transaction.ts @@ -50,6 +50,10 @@ transactionsRouter.get("/", async (req: Request, res: Response) => { } catch (err) { console.error("Failed to fetch transactions", err); return res.status(500).json({ error: "Failed to fetch transactions" }); + } finally { + if (connection) { + connection.release(); + } } }); diff --git a/app/server/services/ts/user/stryker.conf.json b/app/server/services/ts/user/stryker.conf.json deleted file mode 100644 index 725596c..0000000 --- a/app/server/services/ts/user/stryker.conf.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "mutate": ["src/logic/goals.ts"], - "testRunner": "bun", - "plugins": ["stryker-mutator-bun-runner"], - "bun": { - "testFiles": ["./test/goals.test.ts"], - "timeout": 30000 - } -} diff --git a/app/server/services/ts/user/strykerNODE.conf.json b/app/server/services/ts/user/strykerNODE.conf.json deleted file mode 100644 index fd0b1b6..0000000 --- a/app/server/services/ts/user/strykerNODE.conf.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", - "testRunner": "vitest", - "vitest": { - "configFile": "vitest.config.stryker.ts" - }, - "coverageAnalysis": "perTest", - "mutate": ["src/logic/goals.ts"], - "reporters": ["clear-text", "html"], - "concurrency": 4, - "timeoutMS": 10000 -} diff --git a/app/server/services/ts/user/test/accountTests.test.ts b/app/server/services/ts/user/test/accountTests.test.ts new file mode 100644 index 0000000..a5e923a --- /dev/null +++ b/app/server/services/ts/user/test/accountTests.test.ts @@ -0,0 +1,163 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import request from "supertest"; +import express from "express"; +import { accountsRouter } from "../src/routes/account.ts"; + +// mock the database and auth modules +vi.mock("../src/db.ts", () => ({ + pool: { + getConnection: vi.fn(), + }, +})); + +vi.mock("../src/utils/auth.ts", () => ({ + authenticateJWT: vi.fn(), +})); + +vi.mock("../src/CheckUser.ts", () => ({ + checkUserId: vi.fn(), +})); + +// here we import the router and set up the express app after mocking, so that the mocks are used in the router +const app = express(); +app.use(express.json()); +app.use("/accounts", accountsRouter); + +const mockConnection = { + query: vi.fn(), + release: vi.fn(), +}; + +const { pool } = await import("../src/db.ts"); +const { authenticateJWT } = await import("../src/utils/auth.ts"); +const { checkUserId } = await import("../src/CheckUser.ts"); + +//reset +beforeEach(() => { + vi.clearAllMocks(); + pool.getConnection.mockResolvedValue(mockConnection); +}); + +// POST /accounts + +describe("POST /accounts", () => { + it("returns 400 if name is missing", async () => { + const res = await request(app) + .post("/accounts") + .send({ type: "checking", balance: 100, value: 100 }); + + expect(res.status).toBe(400); + expect(res.body.error).toBe("Name is required"); + }); + + it("returns 400 if type is missing", async () => { + const res = await request(app) + .post("/accounts") + .send({ name: "Test", balance: 100, value: 100 }); + + expect(res.status).toBe(400); + expect(res.body.error).toBe("Type is required"); + }); + + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app) + .post("/accounts") + .send({ name: "Test", type: "checking", balance: 100, value: 100 }); + + expect(res.status).toBe(401); + }); + + it("returns 404 if profile not found", async () => { + authenticateJWT.mockReturnValue(1); + mockConnection.query.mockResolvedValueOnce([[]]); + + const res = await request(app) + .post("/accounts") + .send({ name: "Test", type: "checking", balance: 100, value: 100 }); + + expect(res.status).toBe(404); + }); +}); + +// GET /accounts +describe("GET /accounts", () => { + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app).get("/accounts"); + + expect(res.status).toBe(401); + }); + + it("returns 404 if profile not found", async () => { + authenticateJWT.mockReturnValue(1); + mockConnection.query.mockResolvedValueOnce([[]]); // no profile rows + + const res = await request(app).get("/accounts"); + + expect(res.status).toBe(404); + }); +}); + +// PUT /accounts +describe("PUT /accounts", () => { + it("returns 401 if no account is provided", async () => { + const res = await request(app).put("/accounts").send({}); + expect(res.status).toBe(401); + }); + + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app) + .put("/accounts") + .send({ id: 1, name: "x", type: "checking", balance: 10, value: 10 }); + + expect(res.status).toBe(401); + }); + + it("returns 401 if user does not own the account", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(false); + + const res = await request(app) + .put("/accounts") + .send({ id: 1, name: "x", type: "checking", balance: 10, value: 10 }); + + expect(res.status).toBe(401); + }); +}); + +// DELETE /accounts +describe("DELETE /accounts", () => { + it("returns 400 if no id is provided", async () => { + const res = await request(app).delete("/accounts"); + expect(res.status).toBe(400); + }); + + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app).delete("/accounts?id=1"); + expect(res.status).toBe(401); + }); + + it("returns 401 if user does not own the account", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(false); + + const res = await request(app).delete("/accounts?id=1"); + + expect(res.status).toBe(401); + }); +}); diff --git a/app/server/services/ts/user/test/logic_transactions.test.ts b/app/server/services/ts/user/test/logic_transactions.test.ts index d4fe83c..dcce5c3 100644 --- a/app/server/services/ts/user/test/logic_transactions.test.ts +++ b/app/server/services/ts/user/test/logic_transactions.test.ts @@ -8,14 +8,20 @@ import { createEmptyTransactions, createTransactionsWithMissingFields, } from "./factories/transaction.factory.ts"; -import type { Transaction } from "../src/types/Transaction.ts"; +// import type { Transaction } from "../src/types/Transaction.ts"; + +const mockQuery = vi.fn(); +const mockPool = { + query: mockQuery, +} as unknown as Pool; vi.mock("../src/queries/transactions", () => ({ getAllTransactionsQuery: vi.fn(), + getTransactionAccountNames: vi.fn() })); describe("getTransactionsData", () => { - const mockPool = {} as Pool; + // const mockPool = {} as Pool; const userId = "123"; const mockGetAllTransactionsQuery = @@ -28,6 +34,7 @@ describe("getTransactionsData", () => { describe("Happy path", () => { it("should return enriched transactions", async () => { const mockTransactions = createMockTransactions(3); + mockQuery.mockResolvedValue([mockTransactions]); mockGetAllTransactionsQuery.mockResolvedValue(mockTransactions); const result = await getTransactionsData(mockPool, userId); @@ -49,6 +56,7 @@ describe("getTransactionsData", () => { constructor: { name: "RowDataPacket" }, }), ]; + mockQuery.mockResolvedValue([[]]); mockGetAllTransactionsQuery.mockResolvedValue(mockTransactions); const result = await getTransactionsData(mockPool, userId); @@ -60,6 +68,7 @@ describe("getTransactionsData", () => { describe("Edge cases", () => { it("should handle empty transactions", async () => { + mockQuery.mockResolvedValue([[]]); mockGetAllTransactionsQuery.mockResolvedValue(createEmptyTransactions()); const result = await getTransactionsData(mockPool, userId); @@ -68,6 +77,7 @@ describe("getTransactionsData", () => { }); it("should handle missing sender/recipient fields", async () => { + mockQuery.mockResolvedValue([[]]); const mockTransactions = createTransactionsWithMissingFields(); mockGetAllTransactionsQuery.mockResolvedValue(mockTransactions); @@ -86,4 +96,3 @@ describe("getTransactionsData", () => { }); }); }); - diff --git a/app/server/services/ts/user/test/transaction.test.ts b/app/server/services/ts/user/test/transaction.test.ts new file mode 100644 index 0000000..8033666 --- /dev/null +++ b/app/server/services/ts/user/test/transaction.test.ts @@ -0,0 +1,201 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import request from "supertest"; +import express from "express"; +import { transactionsRouter } from "../src/routes/transaction.ts"; + +//mock for database and auth modules +vi.mock("../src/db.ts", () => ({ + pool: { + getConnection: vi.fn(), + }, +})); + +vi.mock("../src/utils/auth.ts", () => ({ + authenticateJWT: vi.fn(), +})); + +vi.mock("../src/CheckUser.ts", () => ({ + checkUserId: vi.fn(), +})); + +vi.mock("../src/DataConversion.ts", () => ({ + convertToDateTime: vi.fn((d) => d), +})); + +const app = express(); +app.use(express.json()); +app.use("/transactions", transactionsRouter); + +const mockConnection = { + query: vi.fn(), + release: vi.fn(), + beginTransaction: vi.fn(), + commit: vi.fn(), + rollback: vi.fn(), +}; + +const { pool } = await import("../src/db.ts"); +const { authenticateJWT } = await import("../src/utils/auth.ts"); +const { checkUserId } = await import("../src/CheckUser.ts"); + +// reset +beforeEach(() => { + vi.clearAllMocks(); + pool.getConnection.mockResolvedValue(mockConnection); +}); + +// GET /transactions +describe("GET /transactions", () => { + it("returns 400 if financialAccount_id is missing", async () => { + const res = await request(app).get("/transactions"); + expect(res.status).toBe(400); + }); + + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app).get("/transactions?financialAccount_id=1"); + expect(res.status).toBe(401); + }); + + it("returns 401 if user does not own the account", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(false); + + const res = await request(app).get("/transactions?financialAccount_id=1"); + expect(res.status).toBe(401); + }); +}); + +// POST /transactions +describe("POST /transactions", () => { + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app) + .post("/transactions") + .send({ financialAccount_id: 1, amount: 10, date: "2024-01-01" }); + + expect(res.status).toBe(401); + }); + + it("returns 401 if user does not own the account", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(false); + + const res = await request(app) + .post("/transactions") + .send({ financialAccount_id: 1, amount: 10, date: "2024-01-01" }); + + expect(res.status).toBe(401); + }); +}); + +// PUT /transactions +describe("PUT /transactions", () => { + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app) + .put("/transactions") + .send({ id: 1, financialAccount_id: 1, amount: 10, date: "2024-01-01" }); + + expect(res.status).toBe(401); + }); + + it("returns 401 if user does not own the account", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(false); + + const res = await request(app) + .put("/transactions") + .send({ id: 1, financialAccount_id: 1, amount: 10, date: "2024-01-01" }); + + expect(res.status).toBe(401); + }); +}); + +// DELETE /transactions +describe("DELETE /transactions", () => { + it("returns 400 if id or financialAccount_id missing", async () => { + const res = await request(app).delete("/transactions").send({}); + expect(res.status).toBe(400); + }); + + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app) + .delete("/transactions") + .send({ id: 1, financialAccount_id: 1 }); + + expect(res.status).toBe(401); + }); + + it("returns 401 if user does not own the account", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(false); + + const res = await request(app) + .delete("/transactions") + .send({ id: 1, financialAccount_id: 1 }); + + expect(res.status).toBe(401); + }); +}); + +// POST /transactions/csvTransaction +describe("POST /transactions/csvTransaction", () => { + it("returns 400 if payload invalid", async () => { + const res = await request(app) + .post("/transactions/csvTransaction") + .send({ financialAccount_id: 1, transactions: "not-array" }); + + expect(res.status).toBe(400); + }); + + it("returns 401 if JWT fails", async () => { + authenticateJWT.mockImplementation(() => { + throw new Error("bad token"); + }); + + const res = await request(app) + .post("/transactions/csvTransaction") + .send({ financialAccount_id: 1, transactions: [] }); + + expect(res.status).toBe(401); + }); + + it("returns 401 if user does not own the account", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(false); + + const res = await request(app) + .post("/transactions/csvTransaction") + .send({ financialAccount_id: 1, transactions: [] }); + + expect(res.status).toBe(401); + }); + + it("returns 400 if no valid rows", async () => { + authenticateJWT.mockReturnValue(1); + checkUserId.mockResolvedValue(true); + + const res = await request(app) + .post("/transactions/csvTransaction") + .send({ + financialAccount_id: 1, + transactions: [{ errors: ["bad"] }], + }); + + expect(res.status).toBe(401); + }); +}); diff --git a/app/server/services/ts/user/vitest.config.stryker.ts b/app/server/services/ts/user/vitest.config.stryker.ts deleted file mode 100644 index 8ac7718..0000000 --- a/app/server/services/ts/user/vitest.config.stryker.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - environment: "node", - include: ["test/**/*.test.ts"], - coverage: { - provider: "v8", - reporter: ["json", "text"], - }, - }, -}); diff --git a/documentation/acceptance tests/goals.md b/documentation/Acceptance/Goals/GoalsAcceptance.md similarity index 100% rename from documentation/acceptance tests/goals.md rename to documentation/Acceptance/Goals/GoalsAcceptance.md diff --git a/documentation/Coding_Style.md b/documentation/Coding_Style.md new file mode 100644 index 0000000..a7ee3d2 --- /dev/null +++ b/documentation/Coding_Style.md @@ -0,0 +1,58 @@ +### General Map + +To follow our application's code, we have divided everything into client and server directories. Client is done in TS with Bun and VIte as its framework. Server consists of microservices in both TS and Python. The database is also located in the server as a separate microservice. + +### Client + +At app -> client you will see all the general directories and files that are used to build up the client. + +At client -> test you will find some unit tests for more complex logic and functionality. These were made as an extra precaution even though it was assumed that we did not need them for our grading. + +Within the client directory, you will find standard bun package and organizational files, as well as all of our setup for Docker. The standard Dockerfile and coker-compose.yml do not run tests and are the typical way to launch the client container. Docker files with the ".test" suffix run client-side unit tests. + +In client -> src you will find all of the client source code, including various directories. The App.tsx file contains some of the first few authentication steps that would be typically seen by a user. This authentication shell wrapps the entirety of the code beyond. + +In src -> api you will find our API managers for various features. Some of these are more functional than others and contain a mixture of methods that would be typically called from a single page instead of just one feature. They all generally follow the structure of using an axios instance from the config.ts file, which appends JWT to every request sent to the server. You will find that all methods handle errors from the response gracefully. + +In src -> components you will find the multitude of components utilized throughout the pages, as well as some separation for CSV reading components, market page components, and some more general ui, which is a remnant of our project's initial stages where we tried to use shadcn/ui components. Most components end up containing a lot of HTML within them and they can get very complex. You will find comments throughtout the code whenever chunks of code must be explained. We did not spend time commenting on self-explanatory, simple functions. + +In src -> enum you will find some enumerator types to assist with account and transaction logic for user data input feature. + +In src -> hooks you will find a single hook for loading in a simple snapshot of user performance in the main dashboard page. + +All of our 5 pages can be found in src -> pages. These are primarily for layout and style and they host a lot of components internally. + +In src -> type you will find all the types that are being used to pass data around the client. + +The src -> utils directory contains all of our helper methods ranging from data validation to formatting. + + +### Server + +This section focuses on everything that can be found in app -> server. Inside the server directory, you will see the same setup of docker-compose files - one for regular deployment and one for running test containers. + +In server -> services you should find the 3 categories for microservices: TS, Python and the database. + +#### Database + +services -> database contains everything related to our MySQL DB. We access the database from outside of its container, so we user a non-root user account that needs extra privileges to contact the db. The populator python script, which can be ran with "python populator.py --clear", as an example, can be used to cleanup and repopulate the databse with randomized data. Our schema can also be found in this directory. + +#### Python + +In services -> python you will find the directories for our analytics and market services. Python naming of variables and methods uses the Python's standard snake case. + +The analytics service is the most complex out of the two, so its source code has been separated into logic, models, queries and utils. The main file, which exists outisde of those directories, contains the handling of redirects and it invokes all the logic code, which then invokes the queries. + +In addition to the rest of the source code for analytics, you can find the test directory, whcih contains unit and integration tests and coverage reports from pytest. Integration tests for all our services can only be ran locally, as they rely on the database being active, which is something that GitHub actions is not flexible enough to support. + +The market service is simpler, as it relies on an external API. All of its source code is found int its own src directory under python -> market -> src. In the market service, you will find the similar unit tests, but also integration tests separated within the integration directory. Unlike other services, market does not rely on the DB, so its integration test can run whenever, even in the CI/CD pipeline. + +#### TypeScript + +Inside the server -> services -> ts directory, you will find all of our TS services: api-gateway, auth and user. We use camel case for all TS naming. + +The auth services is simple in its functionality, so all of its code exsits within auth -> src directory. The files are labeled according to what they contain. + +The api gateway service is the most simple, as it just serves as a redirect for all API calls. It only has an index file as its source code, but that is really all it needs. + +The user microservice is meant to handle all API calls regarding simple data retrieval and processing tasks. It's code has been divided into logic, queries, routes, types and utils. Rerouting and logic invocation is happening in the index file. diff --git a/documentation/Finus_Test_Plan.md b/documentation/Finus_Test_Plan.md index cc74133..f15044c 100644 --- a/documentation/Finus_Test_Plan.md +++ b/documentation/Finus_Test_Plan.md @@ -130,7 +130,21 @@ We attempted to run stryker with vitest in node, but this results in a lot of li 3. ## **Load Testing** -*Skip for Sprint 3\.* +We use a `k6` load test located at `app/server/load-tests/api-gateway-capacity.js` to validate the course capacity requirement: + +- **20 concurrent users** +- **200 total requests per minute** + +The script enforces this by running **20 virtual users** concurrently and having each one issue **10 authenticated requests per minute** after signing up, logging in, and creating a test account. This exercises the gateway, auth service, user service, and database together under sustained load. + +This load test is **not required to be part of the CI/CD pipeline** and can be run manually with Docker when needed. + +```bash +cd app/server +docker compose -f docker-compose.yml -f docker-compose.test.yml up load-test --abort-on-container-failure +``` + +The test fails if request failures rise above 1% or if fewer than 99% of checks pass. 3. # **Terms/Acronyms** From 12095d455d9f1298f4da9c3a574379e3649f83f3 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:17:36 -0500 Subject: [PATCH 194/204] remove dev environment (#98) --- app/server/services/ts/common/expressUtils.ts | 1 - deployment/infrastructure/main.tf | 15 - deployment/infrastructure/terraform.tfstate | 553 +----------------- .../infrastructure/terraform.tfstate.backup | 24 +- 4 files changed, 32 insertions(+), 561 deletions(-) diff --git a/app/server/services/ts/common/expressUtils.ts b/app/server/services/ts/common/expressUtils.ts index f4e313c..0385b08 100644 --- a/app/server/services/ts/common/expressUtils.ts +++ b/app/server/services/ts/common/expressUtils.ts @@ -5,7 +5,6 @@ const origins = [ "http://localhost", "http://localhost:8080", "http://18.190.215.135", //prod webserver - "http://3.142.125.202", //dev webserver ]; export function buildCorsConfig(opts?: { methods?: string[] }) { diff --git a/deployment/infrastructure/main.tf b/deployment/infrastructure/main.tf index 01ada4e..2386d00 100644 --- a/deployment/infrastructure/main.tf +++ b/deployment/infrastructure/main.tf @@ -104,18 +104,3 @@ module "prod" { gateway_id = aws_internet_gateway.default.id route_table_id = aws_route_table.route_table.id } - - -module "dev" { - source = "./modules/finus" - - environment_name = "dev" - ssh_key_pair_name = "finus-dev" - security_group_ids = [aws_security_group.default.id] - webserver_security_group_ids = [aws_security_group.webserver.id] - backend_security_group_ids = [aws_security_group.services.id] - subnet_id = aws_subnet.dev.id - vpc_id = aws_vpc.finus.id - gateway_id = aws_internet_gateway.default.id - route_table_id = aws_route_table.route_table.id -} diff --git a/deployment/infrastructure/terraform.tfstate b/deployment/infrastructure/terraform.tfstate index 8a641c7..ed13165 100644 --- a/deployment/infrastructure/terraform.tfstate +++ b/deployment/infrastructure/terraform.tfstate @@ -1,7 +1,7 @@ { "version": 4, "terraform_version": "1.14.7", - "serial": 122, + "serial": 128, "lineage": "ae3e9505-c9b9-9084-580e-4ba9a7f0ba2f", "outputs": {}, "resources": [ @@ -175,7 +175,21 @@ "attributes": { "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0e428a46b850e036a", "description": "Managed by Terraform", - "egress": [], + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "MySQL from anywhere", + "from_port": 3306, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 3306 + } + ], "id": "sg-0e428a46b850e036a", "ingress": [ { @@ -249,7 +263,7 @@ "owner_id": "177559564758", "region": "us-east-2", "revoke_rules_on_delete": false, - "tags": null, + "tags": {}, "tags_all": {}, "timeouts": null, "vpc_id": "vpc-0e13d5c774ed2b3c8" @@ -421,539 +435,6 @@ } ] }, - { - "module": "module.dev", - "mode": "data", - "type": "aws_ami", - "name": "amazon_linux", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "allow_unsafe_filter": null, - "architecture": "x86_64", - "arn": "arn:aws:ec2:us-east-2::image/ami-0b0b78dcacbab728f", - "block_device_mappings": [ - { - "device_name": "/dev/xvda", - "ebs": { - "delete_on_termination": "true", - "encrypted": "false", - "iops": "3000", - "snapshot_id": "snap-0afc5230d5113aa86", - "throughput": "125", - "volume_initialization_rate": "0", - "volume_size": "8", - "volume_type": "gp3" - }, - "no_device": "", - "virtual_name": "" - } - ], - "boot_mode": "uefi-preferred", - "creation_date": "2026-03-03T18:01:43.000Z", - "deprecation_time": "2026-06-01T23:47:00.000Z", - "description": "Amazon Linux 2023 AMI 2023.10.20260302.1 x86_64 HVM kernel-6.1", - "ena_support": true, - "executable_users": null, - "filter": [ - { - "name": "image-id", - "values": [ - "ami-0b0b78dcacbab728f" - ] - } - ], - "hypervisor": "xen", - "id": "ami-0b0b78dcacbab728f", - "image_id": "ami-0b0b78dcacbab728f", - "image_location": "amazon/al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", - "image_owner_alias": "amazon", - "image_type": "machine", - "imds_support": "v2.0", - "include_deprecated": false, - "kernel_id": "", - "last_launched_time": "", - "most_recent": false, - "name": "al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", - "name_regex": null, - "owner_id": "137112412989", - "owners": null, - "platform": "", - "platform_details": "Linux/UNIX", - "product_codes": [], - "public": true, - "ramdisk_id": "", - "region": "us-east-2", - "root_device_name": "/dev/xvda", - "root_device_type": "ebs", - "root_snapshot_id": "snap-0afc5230d5113aa86", - "sriov_net_support": "simple", - "state": "available", - "state_reason": { - "code": "UNSET", - "message": "UNSET" - }, - "tags": {}, - "timeouts": null, - "tpm_support": "", - "uefi_data": null, - "usage_operation": "RunInstances", - "virtualization_type": "hvm" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "module": "module.dev", - "mode": "managed", - "type": "aws_eip", - "name": "backend", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "address": null, - "allocation_id": "eipalloc-04f46a5b83e1a6af0", - "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-04f46a5b83e1a6af0", - "associate_with_private_ip": null, - "association_id": "eipassoc-0818aa83d85c2f91b", - "carrier_ip": "", - "customer_owned_ip": "", - "customer_owned_ipv4_pool": "", - "domain": "vpc", - "id": "eipalloc-04f46a5b83e1a6af0", - "instance": "i-008147727278f7080", - "ipam_pool_id": null, - "network_border_group": "us-east-2", - "network_interface": "eni-0f364cdcae030e0e6", - "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", - "private_ip": "10.0.221.90", - "ptr_record": "", - "public_dns": "ec2-16-58-118-74.us-east-2.compute.amazonaws.com", - "public_ip": "16.58.118.74", - "public_ipv4_pool": "amazon", - "region": "us-east-2", - "tags": { - "Name": "finus-dev-backend" - }, - "tags_all": { - "Name": "finus-dev-backend" - }, - "timeouts": null - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", - "dependencies": [ - "aws_security_group.default", - "aws_security_group.services", - "aws_subnet.dev", - "aws_vpc.finus", - "module.dev.aws_instance.backend", - "module.dev.data.aws_ami.amazon_linux" - ] - } - ] - }, - { - "module": "module.dev", - "mode": "managed", - "type": "aws_eip", - "name": "webserver", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "address": null, - "allocation_id": "eipalloc-0857b784b20a92dad", - "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0857b784b20a92dad", - "associate_with_private_ip": null, - "association_id": "eipassoc-06e143e0fc9004868", - "carrier_ip": "", - "customer_owned_ip": "", - "customer_owned_ipv4_pool": "", - "domain": "vpc", - "id": "eipalloc-0857b784b20a92dad", - "instance": "i-020ba441d6c3c0258", - "ipam_pool_id": null, - "network_border_group": "us-east-2", - "network_interface": "eni-03283cda15cf431d3", - "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", - "private_ip": "10.0.140.152", - "ptr_record": "", - "public_dns": "ec2-3-142-125-202.us-east-2.compute.amazonaws.com", - "public_ip": "3.142.125.202", - "public_ipv4_pool": "amazon", - "region": "us-east-2", - "tags": { - "Name": "finus-dev-webserver" - }, - "tags_all": { - "Name": "finus-dev-webserver" - }, - "timeouts": null - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", - "dependencies": [ - "aws_security_group.default", - "aws_security_group.webserver", - "aws_subnet.dev", - "aws_vpc.finus", - "module.dev.aws_instance.webserver", - "module.dev.data.aws_ami.amazon_linux" - ] - } - ] - }, - { - "module": "module.dev", - "mode": "managed", - "type": "aws_instance", - "name": "backend", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 2, - "attributes": { - "ami": "ami-0b0b78dcacbab728f", - "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-008147727278f7080", - "associate_public_ip_address": true, - "availability_zone": "us-east-2c", - "capacity_reservation_specification": [ - { - "capacity_reservation_preference": "open", - "capacity_reservation_target": [] - } - ], - "cpu_options": [ - { - "amd_sev_snp": "", - "core_count": 1, - "nested_virtualization": "", - "threads_per_core": 1 - } - ], - "credit_specification": [ - { - "cpu_credits": "standard" - } - ], - "disable_api_stop": false, - "disable_api_termination": false, - "ebs_block_device": [], - "ebs_optimized": false, - "enable_primary_ipv6": null, - "enclave_options": [ - { - "enabled": false - } - ], - "ephemeral_block_device": [], - "force_destroy": false, - "get_password_data": false, - "hibernation": false, - "host_id": "", - "host_resource_group_arn": null, - "iam_instance_profile": "", - "id": "i-008147727278f7080", - "instance_initiated_shutdown_behavior": "stop", - "instance_lifecycle": "", - "instance_market_options": [], - "instance_state": "running", - "instance_type": "t2.nano", - "ipv6_address_count": 0, - "ipv6_addresses": [], - "key_name": "finus-dev", - "launch_template": [], - "maintenance_options": [ - { - "auto_recovery": "default" - } - ], - "metadata_options": [ - { - "http_endpoint": "enabled", - "http_protocol_ipv6": "disabled", - "http_put_response_hop_limit": 2, - "http_tokens": "required", - "instance_metadata_tags": "disabled" - } - ], - "monitoring": false, - "network_interface": [], - "outpost_arn": "", - "password_data": "", - "placement_group": "", - "placement_group_id": "", - "placement_partition_number": 0, - "primary_network_interface": [ - { - "delete_on_termination": true, - "network_interface_id": "eni-0f364cdcae030e0e6" - } - ], - "primary_network_interface_id": "eni-0f364cdcae030e0e6", - "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", - "private_dns_name_options": [ - { - "enable_resource_name_dns_a_record": false, - "enable_resource_name_dns_aaaa_record": false, - "hostname_type": "ip-name" - } - ], - "private_ip": "10.0.221.90", - "public_dns": "", - "public_ip": "16.58.118.74", - "region": "us-east-2", - "root_block_device": [ - { - "delete_on_termination": true, - "device_name": "/dev/xvda", - "encrypted": false, - "iops": 3000, - "kms_key_id": "", - "tags": {}, - "tags_all": {}, - "throughput": 125, - "volume_id": "vol-0c0b46050d4eb986e", - "volume_size": 8, - "volume_type": "gp3" - } - ], - "secondary_network_interface": [], - "secondary_private_ips": [], - "security_groups": [], - "source_dest_check": true, - "spot_instance_request_id": "", - "subnet_id": "subnet-09d5718b3434ec2be", - "tags": { - "Name": "finus-dev-backend" - }, - "tags_all": { - "Name": "finus-dev-backend" - }, - "tenancy": "default", - "timeouts": null, - "user_data": null, - "user_data_base64": null, - "user_data_replace_on_change": false, - "volume_tags": null, - "vpc_security_group_ids": [ - "sg-0c03eb59bd6270fcf", - "sg-0e428a46b850e036a" - ] - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "identity": { - "account_id": "177559564758", - "id": "i-008147727278f7080", - "region": "us-east-2" - }, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", - "dependencies": [ - "aws_security_group.default", - "aws_security_group.services", - "aws_subnet.dev", - "aws_vpc.finus", - "module.dev.data.aws_ami.amazon_linux" - ] - } - ] - }, - { - "module": "module.dev", - "mode": "managed", - "type": "aws_instance", - "name": "webserver", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 2, - "attributes": { - "ami": "ami-0b0b78dcacbab728f", - "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-020ba441d6c3c0258", - "associate_public_ip_address": true, - "availability_zone": "us-east-2c", - "capacity_reservation_specification": [ - { - "capacity_reservation_preference": "open", - "capacity_reservation_target": [] - } - ], - "cpu_options": [ - { - "amd_sev_snp": "", - "core_count": 1, - "nested_virtualization": "", - "threads_per_core": 1 - } - ], - "credit_specification": [ - { - "cpu_credits": "standard" - } - ], - "disable_api_stop": false, - "disable_api_termination": false, - "ebs_block_device": [], - "ebs_optimized": false, - "enable_primary_ipv6": null, - "enclave_options": [ - { - "enabled": false - } - ], - "ephemeral_block_device": [], - "force_destroy": false, - "get_password_data": false, - "hibernation": false, - "host_id": "", - "host_resource_group_arn": null, - "iam_instance_profile": "", - "id": "i-020ba441d6c3c0258", - "instance_initiated_shutdown_behavior": "stop", - "instance_lifecycle": "", - "instance_market_options": [], - "instance_state": "running", - "instance_type": "t2.nano", - "ipv6_address_count": 0, - "ipv6_addresses": [], - "key_name": "finus-dev", - "launch_template": [], - "maintenance_options": [ - { - "auto_recovery": "default" - } - ], - "metadata_options": [ - { - "http_endpoint": "enabled", - "http_protocol_ipv6": "disabled", - "http_put_response_hop_limit": 2, - "http_tokens": "required", - "instance_metadata_tags": "disabled" - } - ], - "monitoring": false, - "network_interface": [], - "outpost_arn": "", - "password_data": "", - "placement_group": "", - "placement_group_id": "", - "placement_partition_number": 0, - "primary_network_interface": [ - { - "delete_on_termination": true, - "network_interface_id": "eni-03283cda15cf431d3" - } - ], - "primary_network_interface_id": "eni-03283cda15cf431d3", - "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", - "private_dns_name_options": [ - { - "enable_resource_name_dns_a_record": false, - "enable_resource_name_dns_aaaa_record": false, - "hostname_type": "ip-name" - } - ], - "private_ip": "10.0.140.152", - "public_dns": "", - "public_ip": "3.142.125.202", - "region": "us-east-2", - "root_block_device": [ - { - "delete_on_termination": true, - "device_name": "/dev/xvda", - "encrypted": false, - "iops": 3000, - "kms_key_id": "", - "tags": {}, - "tags_all": {}, - "throughput": 125, - "volume_id": "vol-09e9f25279b1c2ac1", - "volume_size": 8, - "volume_type": "gp3" - } - ], - "secondary_network_interface": [], - "secondary_private_ips": [], - "security_groups": [], - "source_dest_check": true, - "spot_instance_request_id": "", - "subnet_id": "subnet-09d5718b3434ec2be", - "tags": { - "Name": "finus-dev-webserver" - }, - "tags_all": { - "Name": "finus-dev-webserver" - }, - "tenancy": "default", - "timeouts": null, - "user_data": null, - "user_data_base64": null, - "user_data_replace_on_change": false, - "volume_tags": null, - "vpc_security_group_ids": [ - "sg-0973f01390a9d839a", - "sg-0c03eb59bd6270fcf" - ] - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "identity": { - "account_id": "177559564758", - "id": "i-020ba441d6c3c0258", - "region": "us-east-2" - }, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", - "dependencies": [ - "aws_security_group.default", - "aws_security_group.webserver", - "aws_subnet.dev", - "aws_vpc.finus", - "module.dev.data.aws_ami.amazon_linux" - ] - } - ] - }, - { - "module": "module.dev", - "mode": "managed", - "type": "aws_route_table_association", - "name": "assoc", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "gateway_id": "", - "id": "rtbassoc-04712335fcc923e95", - "region": "us-east-2", - "route_table_id": "rtb-0f24beedbed7e02a3", - "subnet_id": "subnet-09d5718b3434ec2be", - "timeouts": null - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", - "dependencies": [ - "aws_internet_gateway.default", - "aws_route_table.route_table", - "aws_subnet.dev", - "aws_vpc.finus" - ] - } - ] - }, { "module": "module.prod", "mode": "data", diff --git a/deployment/infrastructure/terraform.tfstate.backup b/deployment/infrastructure/terraform.tfstate.backup index cdbe6d6..8a641c7 100644 --- a/deployment/infrastructure/terraform.tfstate.backup +++ b/deployment/infrastructure/terraform.tfstate.backup @@ -1,7 +1,7 @@ { "version": 4, "terraform_version": "1.14.7", - "serial": 117, + "serial": 122, "lineage": "ae3e9505-c9b9-9084-580e-4ba9a7f0ba2f", "outputs": {}, "resources": [ @@ -197,7 +197,7 @@ "owner_id": "177559564758", "region": "us-east-2", "revoke_rules_on_delete": false, - "tags": null, + "tags": {}, "tags_all": {}, "timeouts": null, "vpc_id": "vpc-0e13d5c774ed2b3c8" @@ -223,13 +223,12 @@ "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { - "status": "tainted", "schema_version": 1, "attributes": { - "arn": null, + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0973f01390a9d839a", "description": "Managed by Terraform", "egress": [], - "id": "sg-09cfeb172160ecaa3", + "id": "sg-0973f01390a9d839a", "ingress": [ { "cidr_blocks": [ @@ -239,24 +238,29 @@ "from_port": 80, "ipv6_cidr_blocks": [], "prefix_list_ids": [], - "protocol": "http", + "protocol": "tcp", "security_groups": [], "self": false, "to_port": 80 } ], "name": "finus-webserver", - "name_prefix": null, - "owner_id": null, + "name_prefix": "", + "owner_id": "177559564758", "region": "us-east-2", "revoke_rules_on_delete": false, "tags": null, - "tags_all": null, + "tags_all": {}, "timeouts": null, "vpc_id": "vpc-0e13d5c774ed2b3c8" }, "sensitive_attributes": [], "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "sg-0973f01390a9d839a", + "region": "us-east-2" + }, "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", "dependencies": [ "aws_vpc.finus" @@ -899,6 +903,7 @@ "user_data_replace_on_change": false, "volume_tags": null, "vpc_security_group_ids": [ + "sg-0973f01390a9d839a", "sg-0c03eb59bd6270fcf" ] }, @@ -1431,6 +1436,7 @@ "user_data_replace_on_change": false, "volume_tags": null, "vpc_security_group_ids": [ + "sg-0973f01390a9d839a", "sg-0c03eb59bd6270fcf" ] }, From c5d3bb3710125fd91c21f1d7e31e3aa8c125bf20 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:19:12 -0500 Subject: [PATCH 195/204] fixed client CI failure (#99) --- app/client/.env | 2 +- app/client/docker-compose.yml | 2 +- app/client/src/pages/ProjectionPage.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/client/.env b/app/client/.env index e0ce3dd..aefb610 100644 --- a/app/client/.env +++ b/app/client/.env @@ -3,4 +3,4 @@ #docker-file DOCKER_HUB_USERNAME=measureonecodetwice -RELEASE_VERSION=latest +RELEASE_VERSION=local diff --git a/app/client/docker-compose.yml b/app/client/docker-compose.yml index d995f0b..d1b59e3 100644 --- a/app/client/docker-compose.yml +++ b/app/client/docker-compose.yml @@ -1,6 +1,6 @@ services: webserver-base: - image: finus-webserver-base:local + image: $DOCKER_HUB_USERNAME/finus-webserver-base:$RELEASE_VERSION build: dockerfile: Dockerfile.base diff --git a/app/client/src/pages/ProjectionPage.tsx b/app/client/src/pages/ProjectionPage.tsx index 0d37324..2c54254 100644 --- a/app/client/src/pages/ProjectionPage.tsx +++ b/app/client/src/pages/ProjectionPage.tsx @@ -264,7 +264,7 @@ function ProjectionPage({ session }: ProjectionProp) { useEffect(() => { if (selectedType === "Saving") { - getSaving(session).then((userAccounts) => { + getSaving().then((userAccounts) => { if (userAccounts) setAccounts(userAccounts); }); } From 507e437eabe0dc97abe28b97443719a6d0cc63f0 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:27:10 -0500 Subject: [PATCH 196/204] provision database in terraform (#106) --- deployment/infrastructure/main.tf | 27 +- .../infrastructure/modules/finus/main.tf | 44 ++ deployment/infrastructure/terraform.tfstate | 264 ++++++- .../infrastructure/terraform.tfstate.backup | 739 +++--------------- 4 files changed, 429 insertions(+), 645 deletions(-) diff --git a/deployment/infrastructure/main.tf b/deployment/infrastructure/main.tf index 2386d00..8a6e583 100644 --- a/deployment/infrastructure/main.tf +++ b/deployment/infrastructure/main.tf @@ -1,3 +1,14 @@ +variable "db_username" { + type = string + sensitive = true +} +variable "db_password" { + type = string + sensitive = true +} + + + provider "aws" { region = "us-east-2" } @@ -11,13 +22,15 @@ resource "aws_vpc" "finus" { } resource "aws_subnet" "prod" { - vpc_id = aws_vpc.finus.id - cidr_block = "10.0.0.0/17" + vpc_id = aws_vpc.finus.id + cidr_block = "10.0.0.0/17" + availability_zone = "us-east-2c" } -resource "aws_subnet" "dev" { - vpc_id = aws_vpc.finus.id - cidr_block = "10.0.128.0/17" +resource "aws_subnet" "secondary" { + vpc_id = aws_vpc.finus.id + cidr_block = "10.0.128.0/17" + availability_zone = "us-east-2a" } resource "aws_internet_gateway" "default" { @@ -100,7 +113,11 @@ module "prod" { webserver_security_group_ids = [aws_security_group.webserver.id] backend_security_group_ids = [aws_security_group.services.id] subnet_id = aws_subnet.prod.id + secondary_subnet_id = aws_subnet.secondary.id vpc_id = aws_vpc.finus.id gateway_id = aws_internet_gateway.default.id route_table_id = aws_route_table.route_table.id + + db_username = var.db_username + db_password = var.db_password } diff --git a/deployment/infrastructure/modules/finus/main.tf b/deployment/infrastructure/modules/finus/main.tf index 6ad2b7d..52bd19d 100644 --- a/deployment/infrastructure/modules/finus/main.tf +++ b/deployment/infrastructure/modules/finus/main.tf @@ -16,6 +16,9 @@ variable "backend_security_group_ids" { variable "subnet_id" { type = string } +variable "secondary_subnet_id" { + type = string +} variable "vpc_id" { type = string } @@ -25,6 +28,14 @@ variable "gateway_id" { variable "route_table_id" { type = string } +variable "db_username" { + type = string + sensitive = true +} +variable "db_password" { + type = string + sensitive = true +} data "aws_ami" "amazon_linux" { @@ -81,3 +92,36 @@ resource "aws_eip" "webserver" { Name = join("", ["finus-", var.environment_name, "-webserver"]) } } + +resource "aws_security_group" "db" { + name = "backend access" + vpc_id = var.vpc_id + + ingress { + cidr_blocks = [join("/", [aws_instance.backend.private_ip, "32"])] + from_port = 3306 + to_port = 3306 + protocol = "tcp" + } +} + +resource "aws_db_subnet_group" "default" { + name = "finus-${var.environment_name}-db-subnet-group" + subnet_ids = [var.subnet_id, var.secondary_subnet_id] + + tags = { + Name = "finus-${var.environment_name}-db-subnet-group" + } +} +resource "aws_db_instance" "default" { + allocated_storage = 5 + db_name = "finus" + engine = "mysql" + engine_version = "8.0" + instance_class = "db.t3.micro" + username = var.db_username + password = var.db_password + + vpc_security_group_ids = [aws_security_group.db.id] + db_subnet_group_name = aws_db_subnet_group.default.name +} diff --git a/deployment/infrastructure/terraform.tfstate b/deployment/infrastructure/terraform.tfstate index ed13165..0c07cba 100644 --- a/deployment/infrastructure/terraform.tfstate +++ b/deployment/infrastructure/terraform.tfstate @@ -1,7 +1,7 @@ { "version": 4, "terraform_version": "1.14.7", - "serial": 128, + "serial": 134, "lineage": "ae3e9505-c9b9-9084-580e-4ba9a7f0ba2f", "outputs": {}, "resources": [ @@ -285,23 +285,23 @@ { "mode": "managed", "type": "aws_subnet", - "name": "dev", + "name": "prod", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { "schema_version": 1, "attributes": { - "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-09d5718b3434ec2be", + "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-05e5676ab26e11acc", "assign_ipv6_address_on_creation": false, "availability_zone": "us-east-2c", "availability_zone_id": "use2-az3", - "cidr_block": "10.0.128.0/17", + "cidr_block": "10.0.0.0/17", "customer_owned_ipv4_pool": "", "enable_dns64": false, "enable_lni_at_device_index": 0, "enable_resource_name_dns_a_record_on_launch": false, "enable_resource_name_dns_aaaa_record_on_launch": false, - "id": "subnet-09d5718b3434ec2be", + "id": "subnet-05e5676ab26e11acc", "ipv4_ipam_pool_id": null, "ipv4_netmask_length": null, "ipv6_cidr_block": "", @@ -324,7 +324,7 @@ "identity_schema_version": 0, "identity": { "account_id": "177559564758", - "id": "subnet-09d5718b3434ec2be", + "id": "subnet-05e5676ab26e11acc", "region": "us-east-2" }, "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", @@ -337,23 +337,23 @@ { "mode": "managed", "type": "aws_subnet", - "name": "prod", + "name": "secondary", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { "schema_version": 1, "attributes": { - "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-05e5676ab26e11acc", + "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-0ee7f6fe04be234da", "assign_ipv6_address_on_creation": false, - "availability_zone": "us-east-2c", - "availability_zone_id": "use2-az3", - "cidr_block": "10.0.0.0/17", + "availability_zone": "us-east-2a", + "availability_zone_id": "use2-az1", + "cidr_block": "10.0.128.0/17", "customer_owned_ipv4_pool": "", "enable_dns64": false, "enable_lni_at_device_index": 0, "enable_resource_name_dns_a_record_on_launch": false, "enable_resource_name_dns_aaaa_record_on_launch": false, - "id": "subnet-05e5676ab26e11acc", + "id": "subnet-0ee7f6fe04be234da", "ipv4_ipam_pool_id": null, "ipv4_netmask_length": null, "ipv6_cidr_block": "", @@ -367,7 +367,7 @@ "owner_id": "177559564758", "private_dns_hostname_type_on_launch": "ip-name", "region": "us-east-2", - "tags": {}, + "tags": null, "tags_all": {}, "timeouts": null, "vpc_id": "vpc-0e13d5c774ed2b3c8" @@ -376,7 +376,7 @@ "identity_schema_version": 0, "identity": { "account_id": "177559564758", - "id": "subnet-05e5676ab26e11acc", + "id": "subnet-0ee7f6fe04be234da", "region": "us-east-2" }, "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", @@ -521,6 +521,184 @@ } ] }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_db_instance", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "address": "terraform-20260403140156675200000001.cv8mki0o4cg9.us-east-2.rds.amazonaws.com", + "allocated_storage": 5, + "allow_major_version_upgrade": null, + "apply_immediately": false, + "arn": "arn:aws:rds:us-east-2:177559564758:db:terraform-20260403140156675200000001", + "auto_minor_version_upgrade": true, + "availability_zone": "us-east-2c", + "backup_retention_period": 0, + "backup_target": "region", + "backup_window": "08:07-08:37", + "blue_green_update": [], + "ca_cert_identifier": "rds-ca-rsa2048-g1", + "character_set_name": "", + "copy_tags_to_snapshot": false, + "custom_iam_instance_profile": "", + "customer_owned_ip_enabled": false, + "database_insights_mode": "standard", + "db_name": "finus", + "db_subnet_group_name": "finus-prod-db-subnet-group", + "dedicated_log_volume": false, + "delete_automated_backups": true, + "deletion_protection": false, + "domain": "", + "domain_auth_secret_arn": "", + "domain_dns_ips": null, + "domain_fqdn": "", + "domain_iam_role_name": "", + "domain_ou": "", + "enabled_cloudwatch_logs_exports": null, + "endpoint": "terraform-20260403140156675200000001.cv8mki0o4cg9.us-east-2.rds.amazonaws.com:3306", + "engine": "mysql", + "engine_lifecycle_support": "open-source-rds-extended-support", + "engine_version": "8.0", + "engine_version_actual": "8.0.45", + "final_snapshot_identifier": null, + "hosted_zone_id": "Z2XHWR1WZ565X2", + "iam_database_authentication_enabled": false, + "id": "db-WOTCD5TARTB2FNKCFMRERC3LBA", + "identifier": "terraform-20260403140156675200000001", + "identifier_prefix": "terraform-", + "instance_class": "db.t3.micro", + "iops": 0, + "kms_key_id": "", + "latest_restorable_time": "", + "license_model": "general-public-license", + "listener_endpoint": [], + "maintenance_window": "sat:09:52-sat:10:22", + "manage_master_user_password": null, + "master_user_secret": [], + "master_user_secret_kms_key_id": null, + "max_allocated_storage": 0, + "monitoring_interval": 0, + "monitoring_role_arn": "", + "multi_az": false, + "nchar_character_set_name": "", + "network_type": "IPV4", + "option_group_name": "default:mysql-8-0", + "parameter_group_name": "default.mysql8.0", + "password": "abc123456", + "password_wo": null, + "password_wo_version": null, + "performance_insights_enabled": false, + "performance_insights_kms_key_id": "", + "performance_insights_retention_period": 0, + "port": 3306, + "publicly_accessible": false, + "region": "us-east-2", + "replica_mode": "", + "replicas": [], + "replicate_source_db": "", + "resource_id": "db-WOTCD5TARTB2FNKCFMRERC3LBA", + "restore_to_point_in_time": [], + "s3_import": [], + "skip_final_snapshot": false, + "snapshot_identifier": null, + "status": "available", + "storage_encrypted": false, + "storage_throughput": 0, + "storage_type": "gp2", + "tags": null, + "tags_all": {}, + "timeouts": null, + "timezone": "", + "upgrade_rollout_order": "second", + "upgrade_storage_config": null, + "username": "finus_user", + "vpc_security_group_ids": [ + "sg-0b0da810ad840e612" + ] + }, + "sensitive_attributes": [ + [ + { + "type": "get_attr", + "value": "password" + } + ], + [ + { + "type": "get_attr", + "value": "password_wo" + } + ], + [ + { + "type": "get_attr", + "value": "username" + } + ] + ], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAwLCJkZWxldGUiOjM2MDAwMDAwMDAwMDAsInVwZGF0ZSI6NDgwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.prod", + "aws_subnet.secondary", + "aws_vpc.finus", + "module.prod.aws_db_subnet_group.default", + "module.prod.aws_instance.backend", + "module.prod.aws_security_group.db", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_db_subnet_group", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:rds:us-east-2:177559564758:subgrp:finus-prod-db-subnet-group", + "description": "Managed by Terraform", + "id": "finus-prod-db-subnet-group", + "name": "finus-prod-db-subnet-group", + "name_prefix": "", + "region": "us-east-2", + "subnet_ids": [ + "subnet-05e5676ab26e11acc", + "subnet-0ee7f6fe04be234da" + ], + "supported_network_types": [ + "IPV4" + ], + "tags": { + "Name": "finus-prod-db-subnet-group" + }, + "tags_all": { + "Name": "finus-prod-db-subnet-group" + }, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "bnVsbA==", + "dependencies": [ + "aws_subnet.prod", + "aws_subnet.secondary", + "aws_vpc.finus" + ] + } + ] + }, { "module": "module.prod", "mode": "managed", @@ -967,6 +1145,64 @@ ] } ] + }, + { + "module": "module.prod", + "mode": "managed", + "type": "aws_security_group", + "name": "db", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0b0da810ad840e612", + "description": "Managed by Terraform", + "egress": [], + "id": "sg-0b0da810ad840e612", + "ingress": [ + { + "cidr_blocks": [ + "10.0.50.231/32" + ], + "description": "", + "from_port": 3306, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 3306 + } + ], + "name": "backend access", + "name_prefix": "", + "owner_id": "177559564758", + "region": "us-east-2", + "revoke_rules_on_delete": false, + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0e13d5c774ed2b3c8" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "identity": { + "account_id": "177559564758", + "id": "sg-0b0da810ad840e612", + "region": "us-east-2" + }, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "aws_security_group.default", + "aws_security_group.services", + "aws_subnet.prod", + "aws_vpc.finus", + "module.prod.aws_instance.backend", + "module.prod.data.aws_ami.amazon_linux" + ] + } + ] } ], "check_results": null diff --git a/deployment/infrastructure/terraform.tfstate.backup b/deployment/infrastructure/terraform.tfstate.backup index 8a641c7..edef8a9 100644 --- a/deployment/infrastructure/terraform.tfstate.backup +++ b/deployment/infrastructure/terraform.tfstate.backup @@ -1,7 +1,7 @@ { "version": 4, "terraform_version": "1.14.7", - "serial": 122, + "serial": 130, "lineage": "ae3e9505-c9b9-9084-580e-4ba9a7f0ba2f", "outputs": {}, "resources": [ @@ -175,7 +175,21 @@ "attributes": { "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0e428a46b850e036a", "description": "Managed by Terraform", - "egress": [], + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "MySQL from anywhere", + "from_port": 3306, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 3306 + } + ], "id": "sg-0e428a46b850e036a", "ingress": [ { @@ -249,7 +263,7 @@ "owner_id": "177559564758", "region": "us-east-2", "revoke_rules_on_delete": false, - "tags": null, + "tags": {}, "tags_all": {}, "timeouts": null, "vpc_id": "vpc-0e13d5c774ed2b3c8" @@ -268,58 +282,6 @@ } ] }, - { - "mode": "managed", - "type": "aws_subnet", - "name": "dev", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 1, - "attributes": { - "arn": "arn:aws:ec2:us-east-2:177559564758:subnet/subnet-09d5718b3434ec2be", - "assign_ipv6_address_on_creation": false, - "availability_zone": "us-east-2c", - "availability_zone_id": "use2-az3", - "cidr_block": "10.0.128.0/17", - "customer_owned_ipv4_pool": "", - "enable_dns64": false, - "enable_lni_at_device_index": 0, - "enable_resource_name_dns_a_record_on_launch": false, - "enable_resource_name_dns_aaaa_record_on_launch": false, - "id": "subnet-09d5718b3434ec2be", - "ipv4_ipam_pool_id": null, - "ipv4_netmask_length": null, - "ipv6_cidr_block": "", - "ipv6_cidr_block_association_id": "", - "ipv6_ipam_pool_id": null, - "ipv6_native": false, - "ipv6_netmask_length": null, - "map_customer_owned_ip_on_launch": false, - "map_public_ip_on_launch": false, - "outpost_arn": "", - "owner_id": "177559564758", - "private_dns_hostname_type_on_launch": "ip-name", - "region": "us-east-2", - "tags": {}, - "tags_all": {}, - "timeouts": null, - "vpc_id": "vpc-0e13d5c774ed2b3c8" - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "identity": { - "account_id": "177559564758", - "id": "subnet-09d5718b3434ec2be", - "region": "us-east-2" - }, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", - "dependencies": [ - "aws_vpc.finus" - ] - } - ] - }, { "mode": "managed", "type": "aws_subnet", @@ -422,7 +384,7 @@ ] }, { - "module": "module.dev", + "module": "module.prod", "mode": "data", "type": "aws_ami", "name": "amazon_linux", @@ -508,7 +470,7 @@ ] }, { - "module": "module.dev", + "module": "module.prod", "mode": "managed", "type": "aws_eip", "name": "backend", @@ -518,31 +480,31 @@ "schema_version": 0, "attributes": { "address": null, - "allocation_id": "eipalloc-04f46a5b83e1a6af0", - "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-04f46a5b83e1a6af0", + "allocation_id": "eipalloc-0262f07d6e23594c4", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0262f07d6e23594c4", "associate_with_private_ip": null, - "association_id": "eipassoc-0818aa83d85c2f91b", + "association_id": "eipassoc-0202a50414bdbee94", "carrier_ip": "", "customer_owned_ip": "", "customer_owned_ipv4_pool": "", "domain": "vpc", - "id": "eipalloc-04f46a5b83e1a6af0", - "instance": "i-008147727278f7080", + "id": "eipalloc-0262f07d6e23594c4", + "instance": "i-0dd805bcaa0a0ade3", "ipam_pool_id": null, "network_border_group": "us-east-2", - "network_interface": "eni-0f364cdcae030e0e6", - "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", - "private_ip": "10.0.221.90", + "network_interface": "eni-0d9461ec08710f2d7", + "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", + "private_ip": "10.0.50.231", "ptr_record": "", - "public_dns": "ec2-16-58-118-74.us-east-2.compute.amazonaws.com", - "public_ip": "16.58.118.74", + "public_dns": "ec2-3-150-23-72.us-east-2.compute.amazonaws.com", + "public_ip": "3.150.23.72", "public_ipv4_pool": "amazon", "region": "us-east-2", "tags": { - "Name": "finus-dev-backend" + "Name": "finus-prod-backend" }, "tags_all": { - "Name": "finus-dev-backend" + "Name": "finus-prod-backend" }, "timeouts": null }, @@ -552,16 +514,16 @@ "dependencies": [ "aws_security_group.default", "aws_security_group.services", - "aws_subnet.dev", + "aws_subnet.prod", "aws_vpc.finus", - "module.dev.aws_instance.backend", - "module.dev.data.aws_ami.amazon_linux" + "module.prod.aws_instance.backend", + "module.prod.data.aws_ami.amazon_linux" ] } ] }, { - "module": "module.dev", + "module": "module.prod", "mode": "managed", "type": "aws_eip", "name": "webserver", @@ -571,31 +533,31 @@ "schema_version": 0, "attributes": { "address": null, - "allocation_id": "eipalloc-0857b784b20a92dad", - "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0857b784b20a92dad", + "allocation_id": "eipalloc-06fe359af4fbd5f9b", + "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-06fe359af4fbd5f9b", "associate_with_private_ip": null, - "association_id": "eipassoc-06e143e0fc9004868", + "association_id": "eipassoc-0ce3ab9543feb5d7f", "carrier_ip": "", "customer_owned_ip": "", "customer_owned_ipv4_pool": "", "domain": "vpc", - "id": "eipalloc-0857b784b20a92dad", - "instance": "i-020ba441d6c3c0258", + "id": "eipalloc-06fe359af4fbd5f9b", + "instance": "i-0f897d2768cad0033", "ipam_pool_id": null, "network_border_group": "us-east-2", - "network_interface": "eni-03283cda15cf431d3", - "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", - "private_ip": "10.0.140.152", + "network_interface": "eni-0e2de615da26dd0d9", + "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", + "private_ip": "10.0.57.91", "ptr_record": "", - "public_dns": "ec2-3-142-125-202.us-east-2.compute.amazonaws.com", - "public_ip": "3.142.125.202", + "public_dns": "ec2-18-190-215-135.us-east-2.compute.amazonaws.com", + "public_ip": "18.190.215.135", "public_ipv4_pool": "amazon", "region": "us-east-2", "tags": { - "Name": "finus-dev-webserver" + "Name": "finus-prod-webserver" }, "tags_all": { - "Name": "finus-dev-webserver" + "Name": "finus-prod-webserver" }, "timeouts": null }, @@ -605,16 +567,16 @@ "dependencies": [ "aws_security_group.default", "aws_security_group.webserver", - "aws_subnet.dev", + "aws_subnet.prod", "aws_vpc.finus", - "module.dev.aws_instance.webserver", - "module.dev.data.aws_ami.amazon_linux" + "module.prod.aws_instance.webserver", + "module.prod.data.aws_ami.amazon_linux" ] } ] }, { - "module": "module.dev", + "module": "module.prod", "mode": "managed", "type": "aws_instance", "name": "backend", @@ -624,7 +586,7 @@ "schema_version": 2, "attributes": { "ami": "ami-0b0b78dcacbab728f", - "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-008147727278f7080", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0dd805bcaa0a0ade3", "associate_public_ip_address": true, "availability_zone": "us-east-2c", "capacity_reservation_specification": [ @@ -663,7 +625,7 @@ "host_id": "", "host_resource_group_arn": null, "iam_instance_profile": "", - "id": "i-008147727278f7080", + "id": "i-0dd805bcaa0a0ade3", "instance_initiated_shutdown_behavior": "stop", "instance_lifecycle": "", "instance_market_options": [], @@ -697,11 +659,11 @@ "primary_network_interface": [ { "delete_on_termination": true, - "network_interface_id": "eni-0f364cdcae030e0e6" + "network_interface_id": "eni-0d9461ec08710f2d7" } ], - "primary_network_interface_id": "eni-0f364cdcae030e0e6", - "private_dns": "ip-10-0-221-90.us-east-2.compute.internal", + "primary_network_interface_id": "eni-0d9461ec08710f2d7", + "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", "private_dns_name_options": [ { "enable_resource_name_dns_a_record": false, @@ -709,9 +671,9 @@ "hostname_type": "ip-name" } ], - "private_ip": "10.0.221.90", + "private_ip": "10.0.50.231", "public_dns": "", - "public_ip": "16.58.118.74", + "public_ip": "3.150.23.72", "region": "us-east-2", "root_block_device": [ { @@ -723,7 +685,7 @@ "tags": {}, "tags_all": {}, "throughput": 125, - "volume_id": "vol-0c0b46050d4eb986e", + "volume_id": "vol-0115ad31723e63439", "volume_size": 8, "volume_type": "gp3" } @@ -733,12 +695,12 @@ "security_groups": [], "source_dest_check": true, "spot_instance_request_id": "", - "subnet_id": "subnet-09d5718b3434ec2be", + "subnet_id": "subnet-05e5676ab26e11acc", "tags": { - "Name": "finus-dev-backend" + "Name": "finus-prod-backend" }, "tags_all": { - "Name": "finus-dev-backend" + "Name": "finus-prod-backend" }, "tenancy": "default", "timeouts": null, @@ -755,22 +717,22 @@ "identity_schema_version": 0, "identity": { "account_id": "177559564758", - "id": "i-008147727278f7080", + "id": "i-0dd805bcaa0a0ade3", "region": "us-east-2" }, "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", "dependencies": [ "aws_security_group.default", "aws_security_group.services", - "aws_subnet.dev", + "aws_subnet.prod", "aws_vpc.finus", - "module.dev.data.aws_ami.amazon_linux" + "module.prod.data.aws_ami.amazon_linux" ] } ] }, { - "module": "module.dev", + "module": "module.prod", "mode": "managed", "type": "aws_instance", "name": "webserver", @@ -780,7 +742,7 @@ "schema_version": 2, "attributes": { "ami": "ami-0b0b78dcacbab728f", - "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-020ba441d6c3c0258", + "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0f897d2768cad0033", "associate_public_ip_address": true, "availability_zone": "us-east-2c", "capacity_reservation_specification": [ @@ -819,7 +781,7 @@ "host_id": "", "host_resource_group_arn": null, "iam_instance_profile": "", - "id": "i-020ba441d6c3c0258", + "id": "i-0f897d2768cad0033", "instance_initiated_shutdown_behavior": "stop", "instance_lifecycle": "", "instance_market_options": [], @@ -853,11 +815,11 @@ "primary_network_interface": [ { "delete_on_termination": true, - "network_interface_id": "eni-03283cda15cf431d3" + "network_interface_id": "eni-0e2de615da26dd0d9" } ], - "primary_network_interface_id": "eni-03283cda15cf431d3", - "private_dns": "ip-10-0-140-152.us-east-2.compute.internal", + "primary_network_interface_id": "eni-0e2de615da26dd0d9", + "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", "private_dns_name_options": [ { "enable_resource_name_dns_a_record": false, @@ -865,9 +827,9 @@ "hostname_type": "ip-name" } ], - "private_ip": "10.0.140.152", + "private_ip": "10.0.57.91", "public_dns": "", - "public_ip": "3.142.125.202", + "public_ip": "18.190.215.135", "region": "us-east-2", "root_block_device": [ { @@ -879,7 +841,7 @@ "tags": {}, "tags_all": {}, "throughput": 125, - "volume_id": "vol-09e9f25279b1c2ac1", + "volume_id": "vol-05d61c7cb18058c36", "volume_size": 8, "volume_type": "gp3" } @@ -889,12 +851,12 @@ "security_groups": [], "source_dest_check": true, "spot_instance_request_id": "", - "subnet_id": "subnet-09d5718b3434ec2be", + "subnet_id": "subnet-05e5676ab26e11acc", "tags": { - "Name": "finus-dev-webserver" + "Name": "finus-prod-webserver" }, "tags_all": { - "Name": "finus-dev-webserver" + "Name": "finus-prod-webserver" }, "tenancy": "default", "timeouts": null, @@ -911,22 +873,22 @@ "identity_schema_version": 0, "identity": { "account_id": "177559564758", - "id": "i-020ba441d6c3c0258", + "id": "i-0f897d2768cad0033", "region": "us-east-2" }, "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", "dependencies": [ "aws_security_group.default", "aws_security_group.webserver", - "aws_subnet.dev", + "aws_subnet.prod", "aws_vpc.finus", - "module.dev.data.aws_ami.amazon_linux" + "module.prod.data.aws_ami.amazon_linux" ] } ] }, { - "module": "module.dev", + "module": "module.prod", "mode": "managed", "type": "aws_route_table_association", "name": "assoc", @@ -936,10 +898,10 @@ "schema_version": 0, "attributes": { "gateway_id": "", - "id": "rtbassoc-04712335fcc923e95", + "id": "rtbassoc-0b1e2801d2830d94c", "region": "us-east-2", "route_table_id": "rtb-0f24beedbed7e02a3", - "subnet_id": "subnet-09d5718b3434ec2be", + "subnet_id": "subnet-05e5676ab26e11acc", "timeouts": null }, "sensitive_attributes": [], @@ -948,7 +910,7 @@ "dependencies": [ "aws_internet_gateway.default", "aws_route_table.route_table", - "aws_subnet.dev", + "aws_subnet.prod", "aws_vpc.finus" ] } @@ -956,536 +918,61 @@ }, { "module": "module.prod", - "mode": "data", - "type": "aws_ami", - "name": "amazon_linux", + "mode": "managed", + "type": "aws_security_group", + "name": "db", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { - "schema_version": 0, + "schema_version": 1, "attributes": { - "allow_unsafe_filter": null, - "architecture": "x86_64", - "arn": "arn:aws:ec2:us-east-2::image/ami-0b0b78dcacbab728f", - "block_device_mappings": [ - { - "device_name": "/dev/xvda", - "ebs": { - "delete_on_termination": "true", - "encrypted": "false", - "iops": "3000", - "snapshot_id": "snap-0afc5230d5113aa86", - "throughput": "125", - "volume_initialization_rate": "0", - "volume_size": "8", - "volume_type": "gp3" - }, - "no_device": "", - "virtual_name": "" - } - ], - "boot_mode": "uefi-preferred", - "creation_date": "2026-03-03T18:01:43.000Z", - "deprecation_time": "2026-06-01T23:47:00.000Z", - "description": "Amazon Linux 2023 AMI 2023.10.20260302.1 x86_64 HVM kernel-6.1", - "ena_support": true, - "executable_users": null, - "filter": [ + "arn": "arn:aws:ec2:us-east-2:177559564758:security-group/sg-0b0da810ad840e612", + "description": "Managed by Terraform", + "egress": [], + "id": "sg-0b0da810ad840e612", + "ingress": [ { - "name": "image-id", - "values": [ - "ami-0b0b78dcacbab728f" - ] + "cidr_blocks": [ + "10.0.50.231/32" + ], + "description": "", + "from_port": 3306, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 3306 } ], - "hypervisor": "xen", - "id": "ami-0b0b78dcacbab728f", - "image_id": "ami-0b0b78dcacbab728f", - "image_location": "amazon/al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", - "image_owner_alias": "amazon", - "image_type": "machine", - "imds_support": "v2.0", - "include_deprecated": false, - "kernel_id": "", - "last_launched_time": "", - "most_recent": false, - "name": "al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64", - "name_regex": null, - "owner_id": "137112412989", - "owners": null, - "platform": "", - "platform_details": "Linux/UNIX", - "product_codes": [], - "public": true, - "ramdisk_id": "", + "name": "backend access", + "name_prefix": "", + "owner_id": "177559564758", "region": "us-east-2", - "root_device_name": "/dev/xvda", - "root_device_type": "ebs", - "root_snapshot_id": "snap-0afc5230d5113aa86", - "sriov_net_support": "simple", - "state": "available", - "state_reason": { - "code": "UNSET", - "message": "UNSET" - }, + "revoke_rules_on_delete": false, "tags": {}, + "tags_all": {}, "timeouts": null, - "tpm_support": "", - "uefi_data": null, - "usage_operation": "RunInstances", - "virtualization_type": "hvm" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "module": "module.prod", - "mode": "managed", - "type": "aws_eip", - "name": "backend", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "address": null, - "allocation_id": "eipalloc-0262f07d6e23594c4", - "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-0262f07d6e23594c4", - "associate_with_private_ip": null, - "association_id": "eipassoc-0202a50414bdbee94", - "carrier_ip": "", - "customer_owned_ip": "", - "customer_owned_ipv4_pool": "", - "domain": "vpc", - "id": "eipalloc-0262f07d6e23594c4", - "instance": "i-0dd805bcaa0a0ade3", - "ipam_pool_id": null, - "network_border_group": "us-east-2", - "network_interface": "eni-0d9461ec08710f2d7", - "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", - "private_ip": "10.0.50.231", - "ptr_record": "", - "public_dns": "ec2-3-150-23-72.us-east-2.compute.amazonaws.com", - "public_ip": "3.150.23.72", - "public_ipv4_pool": "amazon", - "region": "us-east-2", - "tags": { - "Name": "finus-prod-backend" - }, - "tags_all": { - "Name": "finus-prod-backend" - }, - "timeouts": null - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", - "dependencies": [ - "aws_security_group.default", - "aws_security_group.services", - "aws_subnet.prod", - "aws_vpc.finus", - "module.prod.aws_instance.backend", - "module.prod.data.aws_ami.amazon_linux" - ] - } - ] - }, - { - "module": "module.prod", - "mode": "managed", - "type": "aws_eip", - "name": "webserver", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "address": null, - "allocation_id": "eipalloc-06fe359af4fbd5f9b", - "arn": "arn:aws:ec2:us-east-2:177559564758:elastic-ip/eipalloc-06fe359af4fbd5f9b", - "associate_with_private_ip": null, - "association_id": "eipassoc-0ce3ab9543feb5d7f", - "carrier_ip": "", - "customer_owned_ip": "", - "customer_owned_ipv4_pool": "", - "domain": "vpc", - "id": "eipalloc-06fe359af4fbd5f9b", - "instance": "i-0f897d2768cad0033", - "ipam_pool_id": null, - "network_border_group": "us-east-2", - "network_interface": "eni-0e2de615da26dd0d9", - "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", - "private_ip": "10.0.57.91", - "ptr_record": "", - "public_dns": "ec2-18-190-215-135.us-east-2.compute.amazonaws.com", - "public_ip": "18.190.215.135", - "public_ipv4_pool": "amazon", - "region": "us-east-2", - "tags": { - "Name": "finus-prod-webserver" - }, - "tags_all": { - "Name": "finus-prod-webserver" - }, - "timeouts": null - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxODAwMDAwMDAwMDAsInJlYWQiOjkwMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMDB9fQ==", - "dependencies": [ - "aws_security_group.default", - "aws_security_group.webserver", - "aws_subnet.prod", - "aws_vpc.finus", - "module.prod.aws_instance.webserver", - "module.prod.data.aws_ami.amazon_linux" - ] - } - ] - }, - { - "module": "module.prod", - "mode": "managed", - "type": "aws_instance", - "name": "backend", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 2, - "attributes": { - "ami": "ami-0b0b78dcacbab728f", - "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0dd805bcaa0a0ade3", - "associate_public_ip_address": true, - "availability_zone": "us-east-2c", - "capacity_reservation_specification": [ - { - "capacity_reservation_preference": "open", - "capacity_reservation_target": [] - } - ], - "cpu_options": [ - { - "amd_sev_snp": "", - "core_count": 1, - "nested_virtualization": "", - "threads_per_core": 1 - } - ], - "credit_specification": [ - { - "cpu_credits": "standard" - } - ], - "disable_api_stop": false, - "disable_api_termination": false, - "ebs_block_device": [], - "ebs_optimized": false, - "enable_primary_ipv6": null, - "enclave_options": [ - { - "enabled": false - } - ], - "ephemeral_block_device": [], - "force_destroy": false, - "get_password_data": false, - "hibernation": false, - "host_id": "", - "host_resource_group_arn": null, - "iam_instance_profile": "", - "id": "i-0dd805bcaa0a0ade3", - "instance_initiated_shutdown_behavior": "stop", - "instance_lifecycle": "", - "instance_market_options": [], - "instance_state": "running", - "instance_type": "t2.nano", - "ipv6_address_count": 0, - "ipv6_addresses": [], - "key_name": "finus-dev", - "launch_template": [], - "maintenance_options": [ - { - "auto_recovery": "default" - } - ], - "metadata_options": [ - { - "http_endpoint": "enabled", - "http_protocol_ipv6": "disabled", - "http_put_response_hop_limit": 2, - "http_tokens": "required", - "instance_metadata_tags": "disabled" - } - ], - "monitoring": false, - "network_interface": [], - "outpost_arn": "", - "password_data": "", - "placement_group": "", - "placement_group_id": "", - "placement_partition_number": 0, - "primary_network_interface": [ - { - "delete_on_termination": true, - "network_interface_id": "eni-0d9461ec08710f2d7" - } - ], - "primary_network_interface_id": "eni-0d9461ec08710f2d7", - "private_dns": "ip-10-0-50-231.us-east-2.compute.internal", - "private_dns_name_options": [ - { - "enable_resource_name_dns_a_record": false, - "enable_resource_name_dns_aaaa_record": false, - "hostname_type": "ip-name" - } - ], - "private_ip": "10.0.50.231", - "public_dns": "", - "public_ip": "3.150.23.72", - "region": "us-east-2", - "root_block_device": [ - { - "delete_on_termination": true, - "device_name": "/dev/xvda", - "encrypted": false, - "iops": 3000, - "kms_key_id": "", - "tags": {}, - "tags_all": {}, - "throughput": 125, - "volume_id": "vol-0115ad31723e63439", - "volume_size": 8, - "volume_type": "gp3" - } - ], - "secondary_network_interface": [], - "secondary_private_ips": [], - "security_groups": [], - "source_dest_check": true, - "spot_instance_request_id": "", - "subnet_id": "subnet-05e5676ab26e11acc", - "tags": { - "Name": "finus-prod-backend" - }, - "tags_all": { - "Name": "finus-prod-backend" - }, - "tenancy": "default", - "timeouts": null, - "user_data": null, - "user_data_base64": null, - "user_data_replace_on_change": false, - "volume_tags": null, - "vpc_security_group_ids": [ - "sg-0c03eb59bd6270fcf", - "sg-0e428a46b850e036a" - ] + "vpc_id": "vpc-0e13d5c774ed2b3c8" }, "sensitive_attributes": [], "identity_schema_version": 0, "identity": { "account_id": "177559564758", - "id": "i-0dd805bcaa0a0ade3", + "id": "sg-0b0da810ad840e612", "region": "us-east-2" }, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", "dependencies": [ "aws_security_group.default", "aws_security_group.services", "aws_subnet.prod", "aws_vpc.finus", + "module.prod.aws_instance.backend", "module.prod.data.aws_ami.amazon_linux" ] } ] - }, - { - "module": "module.prod", - "mode": "managed", - "type": "aws_instance", - "name": "webserver", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 2, - "attributes": { - "ami": "ami-0b0b78dcacbab728f", - "arn": "arn:aws:ec2:us-east-2:177559564758:instance/i-0f897d2768cad0033", - "associate_public_ip_address": true, - "availability_zone": "us-east-2c", - "capacity_reservation_specification": [ - { - "capacity_reservation_preference": "open", - "capacity_reservation_target": [] - } - ], - "cpu_options": [ - { - "amd_sev_snp": "", - "core_count": 1, - "nested_virtualization": "", - "threads_per_core": 1 - } - ], - "credit_specification": [ - { - "cpu_credits": "standard" - } - ], - "disable_api_stop": false, - "disable_api_termination": false, - "ebs_block_device": [], - "ebs_optimized": false, - "enable_primary_ipv6": null, - "enclave_options": [ - { - "enabled": false - } - ], - "ephemeral_block_device": [], - "force_destroy": false, - "get_password_data": false, - "hibernation": false, - "host_id": "", - "host_resource_group_arn": null, - "iam_instance_profile": "", - "id": "i-0f897d2768cad0033", - "instance_initiated_shutdown_behavior": "stop", - "instance_lifecycle": "", - "instance_market_options": [], - "instance_state": "running", - "instance_type": "t2.nano", - "ipv6_address_count": 0, - "ipv6_addresses": [], - "key_name": "finus-dev", - "launch_template": [], - "maintenance_options": [ - { - "auto_recovery": "default" - } - ], - "metadata_options": [ - { - "http_endpoint": "enabled", - "http_protocol_ipv6": "disabled", - "http_put_response_hop_limit": 2, - "http_tokens": "required", - "instance_metadata_tags": "disabled" - } - ], - "monitoring": false, - "network_interface": [], - "outpost_arn": "", - "password_data": "", - "placement_group": "", - "placement_group_id": "", - "placement_partition_number": 0, - "primary_network_interface": [ - { - "delete_on_termination": true, - "network_interface_id": "eni-0e2de615da26dd0d9" - } - ], - "primary_network_interface_id": "eni-0e2de615da26dd0d9", - "private_dns": "ip-10-0-57-91.us-east-2.compute.internal", - "private_dns_name_options": [ - { - "enable_resource_name_dns_a_record": false, - "enable_resource_name_dns_aaaa_record": false, - "hostname_type": "ip-name" - } - ], - "private_ip": "10.0.57.91", - "public_dns": "", - "public_ip": "18.190.215.135", - "region": "us-east-2", - "root_block_device": [ - { - "delete_on_termination": true, - "device_name": "/dev/xvda", - "encrypted": false, - "iops": 3000, - "kms_key_id": "", - "tags": {}, - "tags_all": {}, - "throughput": 125, - "volume_id": "vol-05d61c7cb18058c36", - "volume_size": 8, - "volume_type": "gp3" - } - ], - "secondary_network_interface": [], - "secondary_private_ips": [], - "security_groups": [], - "source_dest_check": true, - "spot_instance_request_id": "", - "subnet_id": "subnet-05e5676ab26e11acc", - "tags": { - "Name": "finus-prod-webserver" - }, - "tags_all": { - "Name": "finus-prod-webserver" - }, - "tenancy": "default", - "timeouts": null, - "user_data": null, - "user_data_base64": null, - "user_data_replace_on_change": false, - "volume_tags": null, - "vpc_security_group_ids": [ - "sg-0973f01390a9d839a", - "sg-0c03eb59bd6270fcf" - ] - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "identity": { - "account_id": "177559564758", - "id": "i-0f897d2768cad0033", - "region": "us-east-2" - }, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", - "dependencies": [ - "aws_security_group.default", - "aws_security_group.webserver", - "aws_subnet.prod", - "aws_vpc.finus", - "module.prod.data.aws_ami.amazon_linux" - ] - } - ] - }, - { - "module": "module.prod", - "mode": "managed", - "type": "aws_route_table_association", - "name": "assoc", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "gateway_id": "", - "id": "rtbassoc-0b1e2801d2830d94c", - "region": "us-east-2", - "route_table_id": "rtb-0f24beedbed7e02a3", - "subnet_id": "subnet-05e5676ab26e11acc", - "timeouts": null - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjEyMDAwMDAwMDAwMH19", - "dependencies": [ - "aws_internet_gateway.default", - "aws_route_table.route_table", - "aws_subnet.prod", - "aws_vpc.finus" - ] - } - ] } ], "check_results": null From 3cdf59fdbc44977067dbec766c42f2fb94cbae55 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:27:27 -0500 Subject: [PATCH 197/204] removed flaky test (#107) --- app/server/services/ts/user/test/dates.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/server/services/ts/user/test/dates.test.ts b/app/server/services/ts/user/test/dates.test.ts index 37c9593..aed3fb1 100644 --- a/app/server/services/ts/user/test/dates.test.ts +++ b/app/server/services/ts/user/test/dates.test.ts @@ -202,19 +202,4 @@ describe("generateDateRange", () => { ]); }); }); - - describe("Performance", () => { - it("should handle large ranges efficiently", () => { - const start = new Date("2000-01-01"); - const end = new Date("2024-12-31"); - const period = "m"; - - const startTime = performance.now(); - const result = generateDateRange(start, end, period); - const endTime = performance.now(); - - expect(endTime - startTime).toBeLessThan(100); - expect(result.length).toBeGreaterThan(0); - }); - }); }); From 297de533c5f8b6f374f1c0f6d1d9189d6bdecafb Mon Sep 17 00:00:00 2001 From: JackieMei3 <129424667+JackieMei3@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:12:41 -0500 Subject: [PATCH 198/204] Acceptance test (#105) * Fixes not being able to add transactions if you start with no transactions. * Fixed the transation creation and editing the amount input field * Created acceptance tests for projection, dashboard, and user authentication --------- Co-authored-by: Jackie Mei --- .../src/components/TransactionTable.tsx | 50 +++++++------- .../Dashboard/DashboardAcceptance.md | 66 +++++++++++++++++++ .../Projection/ProjectionAcceptance.md | 34 ++++++++++ .../Acceptance/UserAuth/UserAuthAcceptance.md | 27 ++++++++ 4 files changed, 154 insertions(+), 23 deletions(-) create mode 100644 documentation/Acceptance/Dashboard/DashboardAcceptance.md create mode 100644 documentation/Acceptance/Projection/ProjectionAcceptance.md create mode 100644 documentation/Acceptance/UserAuth/UserAuthAcceptance.md diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index 98af1de..0d3d0ec 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -7,16 +7,20 @@ import { } from "../api/DashboardAPI.ts"; import type { Transaction } from "../types/Transaction"; import { - AiOutlineTransaction, AiOutlineSearch, AiOutlinePlus, AiOutlineClose, AiOutlineUpload, } from "react-icons/ai"; import { FiEdit2 } from "react-icons/fi"; -import NoItemState from "./NoItemState"; import type { MinimizedAccount } from "@/types/AccountType.ts"; import CSVImportModal from "./CSVImportModal"; +import { + handleCurrencyBlur, + handleCurrencyChange, + handleListCurrencyBlur, + handleListCurrencyChange, +} from "@/utils/handleInput.ts"; interface TransactionTableProps { initialLimit?: number; @@ -224,11 +228,13 @@ function TransactionTable({ recipient: "", }); + const [newAmount, setNewAmount] = useState(""); + const handleAddTransaction = async () => { if ( !newTransaction.date || !newTransaction.category || - newTransaction.amount === undefined + newAmount === undefined ) { alert("Please fill in required fields (date, category, amount)"); return; @@ -242,6 +248,7 @@ function TransactionTable({ try { const created = await createTransaction({ ...newTransaction, + amount: Number(newAmount), financialAccount_id: selectedAccountId as number, } as Transaction); @@ -283,16 +290,6 @@ function TransactionTable({ ); } - if (allTransactions.length === 0) { - return ( - } - /> - ); - } - return (
{/* Search Bar */} @@ -394,14 +391,12 @@ function TransactionTable({ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" /> - setNewTransaction({ - ...newTransaction, - amount: parseFloat(e.target.value), - }) + value={newAmount || ""} + onChange={(e) => handleCurrencyChange(e, setNewAmount)} + onBlur={(e) => + handleCurrencyBlur(e, e.target.value, setNewAmount) } className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white" /> @@ -592,13 +587,22 @@ function TransactionTable({ Amount - handleFieldChange( + handleListCurrencyChange( + e, + handleFieldChange, + tx.id, + "amount", + ) + } + onBlur={(e) => + handleListCurrencyBlur( + e, + handleFieldChange, tx.id, "amount", - parseFloat(e.target.value), ) } className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm" diff --git a/documentation/Acceptance/Dashboard/DashboardAcceptance.md b/documentation/Acceptance/Dashboard/DashboardAcceptance.md new file mode 100644 index 0000000..d542df8 --- /dev/null +++ b/documentation/Acceptance/Dashboard/DashboardAcceptance.md @@ -0,0 +1,66 @@ +# Acceptance Criteria + +## 1: Visualization of the user's income, investments and savings +- The user is presented with a dashboard, which contains statistical graphs +- One of the graphs displays a visualization of the user's expenses over time +- One of the graph displays a visualizatoin of the user's saving contributed over time +- One of the graph shows the relationship between the total income and where all of that money ends up going + +## 2: Tracking the user's budget goals +- Users are proposed a feasible budget plan +- The budget plan takes into account of the user's income and expenses +- The budget prioritizes saving money +- The user is shown how closely they match the budget and whether they overspent within a defined period of time + +--- + +# Acceptance Test Scenarios + +## 1: Seeing a visual of the user's expenses over time +1. Login in or creates a finus account +2. Go to the account list and click on add account +3. Fill out the account form that appeared with any input except with the type: Credit Card subtype: loan. +4. Click create account and observe the account appearing under the account list. +5. Click on create transaction under the transaction list +6. Fill out the transaction form with account that was created as the selected account. Amount has be in the negatives. +7. Repeat step 5-6 until you have have a multiple transaction within this past week and multiple multiple the last month and multiple in different months in the past year. +8. Go top of the dashboard and select expenses if not already selected. +9. Observe if the amount total in each day matches the amount entered in the transactions. +10. Click on month button and checks if total amount each day within a month from now, matches amount that was entered in the transactions. +11. Click on year button and checks if the total amount each month matches the total amount for transactions entered that month + +## 2: Seeing a visual of the user's savings over time +1. Login or create a finus account +2. Go to the account list and click create account +3. Fill out the form and make an account type of savings, other inputs can be anything. +4. Click on create account observe it being added the account list +5. Click on create transaction under transaction list +6. Fill out the transaction form with the newly created account as the selected account. +7. Repeat step 5-6 until you have a multiple transactions thats within the past week, month and year. +8. Go to the first graph in the dashboard, and click on savings if it's not already selected +9. Click on week button and check if savings amount changes matches the transactions that were created within a week from now +10. Click on month button and check if saving amount changes matches the transactions that were created within a month from now +11. Click on year button and check if saving amount changes matches the transactions that were created within a year from now + +## 3: Seeing a visual of the user's income over time +1. Login or create a finus account +2. Go to the account list and click create account +3. Fill out the form and make an account type of savings, other inputs can be anything. +4. Click on create account observe it being added the account list +5. Click on create transactions under transaction list +6. Fill out the transaction form with the newly created as the selected account. Use consistent categories for the transaction +7. Repeat step 5-6 until you have multiple transactions thats within the past week, month and year. +8. Go the first graph in the dashboard and click on income flow if its not selected already. +9. Click on week and check if the total amount for a transaction categories on the left side of the graph matches the total of the positive transactions(income) of that category within the past week. +10. Check if total amount for a transaction category on right matches the total amount the negative transactions(expenses) of that category with in the past week. +11. Repeat 9 and 10 except for with with month and year, check if transactions with the month/year from now match. + +## 4: Tracking if the user is keeping within budget +1. Login or create a finus account +2. Go to the account list and click create account +3. Fill out the form and make an account type of savings, other inputs can be anything +4. Click on create account observe it being added to the account list. +5. Click on create a transaction under transaction list +6. Fill out the transaction form with the newly created account. Create a transactions of the same type. +7. Repeat step 5-6 until you have multiple transactions thats wtihin the past week, month, and year. +8. Go the budget vs expenditure graph and check if the budget amount matches the total amount of possitive transactions and actual spending amount matches the total amount of negative transactions of that type. \ No newline at end of file diff --git a/documentation/Acceptance/Projection/ProjectionAcceptance.md b/documentation/Acceptance/Projection/ProjectionAcceptance.md new file mode 100644 index 0000000..004bf83 --- /dev/null +++ b/documentation/Acceptance/Projection/ProjectionAcceptance.md @@ -0,0 +1,34 @@ +# Acceptance Criteria + +## 1: Debt Deadlines +- User can input various kinds of debt like loans +- Certain kinds of debt have additional inputs associated with them, like a amortization +- User are presetned with a projection of how much they will owe at every specific period of time, givne minimum payments. + +## 2: Savings compound overtime +- User can input financial information about their savings accounts +- users can specify the kind of account they're using for savings +- User are presented with a graph that visualizes how their savings compound over time +- The projected savings graph shows both worst-case and best-case growth based on historical economic values. + +--- + +# Acceptance Test Scanario + +## 1: Seeing a projection of the user's savings account growth +1. Sign in or create a finus account +2. Create an financial account of savings type +3. Go to the projection page by clicking top left button and then clicking projection +4. Select the saving projection button if not selected already. +5. Select the saving account that was just created. +6. Fill out the rest of the form in saving account detail then click on calculate projection +7. Go to the saving growth projection, you should see a graph with 3 different lines, green being a best case(optimistic), yellow being expected case, and red being worst case(conservative). They best case should show the highest amount in their projection followed up by expected and then worst case. + +## 2: Seeing a projection of how long and how much it would take to pay off a debt +1. Sign in or create a finus account +2. Go to account list and create an account of type Credit Card with subtype loan. +3. Go to the projection page by clicking on the top left and clicking projection. +4. Click on Debt payoff projection if not selected already. +5. Select the account that we created and fill out the rest of the form then click on calculate projections. +6. Go the debt payoff schedule and you will see a line of amount of debt remaining in future payment deadlines. +7. Right bellow the graph, you can see how much interest you paid in total. \ No newline at end of file diff --git a/documentation/Acceptance/UserAuth/UserAuthAcceptance.md b/documentation/Acceptance/UserAuth/UserAuthAcceptance.md new file mode 100644 index 0000000..2b8160c --- /dev/null +++ b/documentation/Acceptance/UserAuth/UserAuthAcceptance.md @@ -0,0 +1,27 @@ +# Acceptance Criteria + +## 1: Use of app on any of my device +- Users can create an account with an email and password +- User data is associated with an account under a certain name +- User can log into their account from mutliple supported devices (Desktop only) +- User can log out of their account and log in with a different accout on the same machine + +--- + +# Acceptance Test + +## 1: Creating mutliple finus accounts +1. Click on sign up +2. Fill out the form, age being 21 and over, email following name@domain.com format, password being at least 8 characters long, including at least both a number and a letter/symbol. +3. Click on create account +4. Should be sent to dashboard page if account creation was a success. +5. Check dashboard displays hello [name] matches the name entered when creating an account +6. Use another device or open incognito tab and try to login into the account that was just created +7. Click on button on top left of the screen and then click on signout, should be sent back to the login page. +8. Repeat steps 1-5 and see if user is able to create and login into another account + +## 2: Login into same finus accounts from different devices +1. Create a finus accounts if not already created +2. login into the finus account +3. Use a different device or open a incognito tab to login into the same account +4. Check if both devices are stilled logged into the account and shows the same info on screen. From 1a0dedabcd29b4a4567c7b5807b2af44fce4e05e Mon Sep 17 00:00:00 2001 From: JackieMei3 <129424667+JackieMei3@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:30:49 -0500 Subject: [PATCH 199/204] Transaction table fix (#110) * Fixes not being able to add transactions if you start with no transactions. * Fixed the transation creation and editing the amount input field * Created acceptance tests for projection, dashboard, and user authentication * Re added back the no item state and now works --------- Co-authored-by: Jackie Mei --- .../src/components/TransactionTable.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index 0d3d0ec..c68c1b4 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -12,6 +12,7 @@ import { AiOutlineClose, AiOutlineUpload, } from "react-icons/ai"; +import NoItemState from "./NoItemState.tsx"; import { FiEdit2 } from "react-icons/fi"; import type { MinimizedAccount } from "@/types/AccountType.ts"; import CSVImportModal from "./CSVImportModal"; @@ -290,6 +291,27 @@ function TransactionTable({ ); } + if (allTransactions.length === 0 && !showAddForm) { + return ( +
+ } + /> + +
+ +
+
+ ); +} + return (
{/* Search Bar */} From 7a91e1df24a5db51d381f35d7b034ea30dc0211b Mon Sep 17 00:00:00 2001 From: Hoang Huy Truong <160953647+HuyTruong24@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:29:30 -0500 Subject: [PATCH 200/204] Fix/integration test runner (#109) * removed node modules in server directory * re-worked how tests are run * modification to formatting hook * removed node modules in server directory * updated workflow runner * fixed tests, removed duplicate test file * cleanup test logging * commit to test pipeline * remove server reset key & change run test action targets * made run_tests.sh executable * Server: Added integration tests for auth * Formatted * Server: created setup for integration tests in analytics service * Sever: Added integration test for compound interest endpoint * Server: more integration tests * address sonarqube issues * Config mutation tests * Eslint checked * Corrected test docker file to intall python test dependencies from requirements-test.txt * Cancelled running mutation tests for TS - stryker does not work on bun * added back the python fixtures for unit tests * Comment out ananlytics mutation test * unit test logic fix --------- Co-authored-by: Logan Co-authored-by: Roman Tebel --- app/server/docker-compose.test.yml | 28 ++- .../analytics/Dockerfile.integration.test | 13 ++ .../python/analytics/Dockerfile.mutation.test | 21 ++ .../services/python/analytics/Dockerfile.test | 4 +- .../python/analytics/integration/conftest.py | 192 ++++++++++++++++++ .../integration/test_debts_integration.py | 137 +++++++++++++ .../integration/test_savings_integration.py | 112 ++++++++++ .../services/python/analytics/pyproject.toml | 8 + .../python/analytics/requirements-test.txt | 5 +- .../python/analytics/requirements.txt | 8 +- .../python/analytics/src/logic/debt.py | 2 +- .../services/python/analytics/src/main.py | 2 +- .../python/analytics/src/models/__init__.py | 0 .../python/analytics/src/models/schemas.py | 14 +- .../python/analytics/test/budgetPerfCalc.py | 152 -------------- .../analytics/test/budget_integration.py | 38 ---- .../python/analytics/test/cat_classifier.py | 55 ----- .../python/analytics/test/incomeflow.py | 131 ------------ .../python/analytics/test/periodCalc.py | 146 ------------- .../services/python/analytics/test/savings.py | 99 --------- .../python/analytics/test/test_savings.py | 2 +- .../ts/auth/Dockerfile.integration.test | 10 + .../services/ts/auth/Dockerfile.mutation.test | 15 ++ app/server/services/ts/auth/bun.lock | 41 ++++ .../services/ts/auth/integration/auth.test.ts | 157 ++++++++++++++ app/server/services/ts/auth/package.json | 6 +- app/server/services/ts/auth/stryker.conf.js | 6 + app/server/services/ts/auth/vitest.config.ts | 9 + app/server/services/ts/bun.lock | 41 ++++ app/server/services/ts/package.json | 1 + 30 files changed, 814 insertions(+), 641 deletions(-) create mode 100644 app/server/services/python/analytics/Dockerfile.integration.test create mode 100644 app/server/services/python/analytics/Dockerfile.mutation.test create mode 100644 app/server/services/python/analytics/integration/conftest.py create mode 100644 app/server/services/python/analytics/integration/test_debts_integration.py create mode 100644 app/server/services/python/analytics/integration/test_savings_integration.py create mode 100644 app/server/services/python/analytics/pyproject.toml create mode 100644 app/server/services/python/analytics/src/models/__init__.py delete mode 100644 app/server/services/python/analytics/test/budgetPerfCalc.py delete mode 100644 app/server/services/python/analytics/test/budget_integration.py delete mode 100644 app/server/services/python/analytics/test/cat_classifier.py delete mode 100644 app/server/services/python/analytics/test/incomeflow.py delete mode 100644 app/server/services/python/analytics/test/periodCalc.py delete mode 100644 app/server/services/python/analytics/test/savings.py create mode 100644 app/server/services/ts/auth/Dockerfile.integration.test create mode 100644 app/server/services/ts/auth/Dockerfile.mutation.test create mode 100644 app/server/services/ts/auth/integration/auth.test.ts create mode 100644 app/server/services/ts/auth/stryker.conf.js create mode 100644 app/server/services/ts/auth/vitest.config.ts diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml index 0dca070..fa92e8c 100644 --- a/app/server/docker-compose.test.yml +++ b/app/server/docker-compose.test.yml @@ -22,18 +22,28 @@ services: dockerfile: Dockerfile.test # # ------ MUTATION TESTS ------ + # THIS CAN'T RUN WITH STRYKER UNLESS WE MOVE TO NODE # user-mutation-test: # build: # context: ./services/ts # dockerfile: user/Dockerfile.mutation.test + # auth-mutation-test: + # build: + # context: ./services/ts + # dockerfile: auth/Dockerfile.mutation.test + market-mutation-test: build: context: services/python/market dockerfile: Dockerfile.mutation.test + # analytics-mutation-test: + # build: + # context: services/python/analytics + # dockerfile: Dockerfile.mutation.test + # MISSING - # ANALYTICS # AUTH # ------ INTEGRATION TESTS ------ @@ -66,3 +76,19 @@ services: - LOAD_TEST_DURATION=2m command: run /scripts/api-gateway-capacity.js + + auth-integration-test: + build: + context: ./services/ts + dockerfile: auth/Dockerfile.integration.test + env_file: + - ./default_container.env + + analytics-integration-test: + build: + context: ./services/python/analytics + dockerfile: Dockerfile.integration.test + env_file: + - ./default_container.env + environment: + - JWT_SECRET diff --git a/app/server/services/python/analytics/Dockerfile.integration.test b/app/server/services/python/analytics/Dockerfile.integration.test new file mode 100644 index 0000000..8b1f0e8 --- /dev/null +++ b/app/server/services/python/analytics/Dockerfile.integration.test @@ -0,0 +1,13 @@ +FROM measureonecodetwice/finus-analytics:local + +WORKDIR /app + +COPY integration ./integration +COPY requirements.txt . +COPY requirements-test.txt . + +RUN pip install -r requirements-test.txt + +ENV CI=true + +CMD ["python", "-m", "pytest", "integration/", "-v"] diff --git a/app/server/services/python/analytics/Dockerfile.mutation.test b/app/server/services/python/analytics/Dockerfile.mutation.test new file mode 100644 index 0000000..9d7a14d --- /dev/null +++ b/app/server/services/python/analytics/Dockerfile.mutation.test @@ -0,0 +1,21 @@ +FROM measureonecodetwice/finus-analytics:local + +WORKDIR /app + +COPY src ./src +COPY test ./test +COPY requirements.txt . +COPY requirements-test.txt . +COPY pyproject.toml . + +RUN pip install -r requirements-test.txt + +RUN cp -r src analytics_src +RUN cp -r test mutation_test +RUN find mutation_test -type f -name "*.py" -exec perl -0pi -e "s/\\bfrom src import\\b/from analytics_src import/g; s/\\bimport src\\./import analytics_src./g; s/\\bsrc\\./analytics_src./g" {} + +RUN rm -rf src test + +ENV PYTHONPATH=/app +ENV CI=true + +CMD ["mutmut", "run"] diff --git a/app/server/services/python/analytics/Dockerfile.test b/app/server/services/python/analytics/Dockerfile.test index cc722a0..0daf46b 100644 --- a/app/server/services/python/analytics/Dockerfile.test +++ b/app/server/services/python/analytics/Dockerfile.test @@ -3,9 +3,9 @@ FROM measureonecodetwice/finus-analytics:local WORKDIR /app/server/services/python/analytics COPY test ./test -COPY requirements.txt . +COPY requirements-test.txt . -RUN pip install -r requirements.txt +RUN pip install -r requirements-test.txt ENV PYTHONPATH=/app/server/services/python/analytics ENV JWT_SECRET=test_secret_key_12345 diff --git a/app/server/services/python/analytics/integration/conftest.py b/app/server/services/python/analytics/integration/conftest.py new file mode 100644 index 0000000..6a83ef3 --- /dev/null +++ b/app/server/services/python/analytics/integration/conftest.py @@ -0,0 +1,192 @@ +from typing import Dict +import os +import time +import random +import string +import re +import httpx +import pytest +from datetime import datetime, timedelta + +ANALYTICS_SERVICE_ADDR = os.getenv("ANALYTICS_SERVICE_ADDR", "http://analytics:8000") +USER_SERVICE_ADDR = os.getenv("USER_SERVICE_ADDR", "http://user:8000") +AUTH_SERVICE_ADDR = os.getenv("AUTH_SERVICE_ADDR", "http://auth:8000") +READY_TIMEOUT_SECONDS = float(os.getenv("ANALYTICS_READY_TIMEOUT_SECONDS", "60")) + +def wait_for_service(base_url: str, timeout_seconds: float) -> None: + deadline = time.monotonic() + timeout_seconds + last_error = "service never became reachable" + + while time.monotonic() < deadline: + try: + response = httpx.get(f"{base_url}/health", timeout=5.0) + if response.status_code == 200 and response.text == "ok": + return + last_error = f"unexpected health response: {response.status_code} {response.text}" + except httpx.HTTPError as exc: + last_error = str(exc) + + time.sleep(1) + + pytest.fail(f"Market service was not ready within {timeout_seconds}s: {last_error}") + +@pytest.fixture(scope="session") +def auth_client() -> httpx.Client: + wait_for_service(AUTH_SERVICE_ADDR, READY_TIMEOUT_SECONDS) + with httpx.Client(base_url=AUTH_SERVICE_ADDR, timeout=15.0) as client: + yield client + +@pytest.fixture(scope="session") +def user_client() -> httpx.Client: + wait_for_service(USER_SERVICE_ADDR, READY_TIMEOUT_SECONDS) + with httpx.Client(base_url=USER_SERVICE_ADDR, timeout=15.0) as client: + yield client + +@pytest.fixture(scope="session") +def analytics_client() -> httpx.Client: + wait_for_service(ANALYTICS_SERVICE_ADDR, READY_TIMEOUT_SECONDS) + + with httpx.Client(base_url=ANALYTICS_SERVICE_ADDR, timeout=15.0) as client: + yield client + +def sleep(ms: int): + time.sleep(ms / 1000) + + +def create_signup_body(prefix: str) -> dict: + safe_prefix = re.sub(r"[^\w]", "", prefix) + unique = f"{safe_prefix}{int(time.time() * 1000)}{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" + + return { + "username": unique, + "email": f"{unique}@sample.com", + "first_name": "Test", + "last_name": "User", + "age": 30, + "password": "123ABC!7", + } + + +def create_authenticated_account( + auth_client: httpx.Client, + user_client: httpx.Client, + prefix: str, + account_overrides: dict = None, +): + deadline = time.time() + 20 + last_failure = "setup did not start" + + account_overrides = account_overrides or {} + + while time.time() < deadline: + signup_body = create_signup_body(prefix) + + try: + # Signup + signup = auth_client.post("/signup", json=signup_body) + if signup.status_code != 201: + last_failure = f"signup={signup.status_code} {signup.text}" + sleep(500) + continue + + # Login + login = auth_client.post( + "/login", + json={ + "email": signup_body["email"], + "password": signup_body["password"], + }, + ) + + login_json = login.json() if login.content else {} + + if login.status_code != 200 or "token" not in login_json: + last_failure = f"login={login.status_code} {login.text}" + sleep(500) + continue + + token = login_json["token"] + + # Create account + account_payload = { + "name": account_overrides.get("name", "Sample Account"), + "type": account_overrides.get("type", "savings"), + "balance": account_overrides.get("balance", 1000), + "value": account_overrides.get("value", 1000), + "subtype": account_overrides.get("subtype", "RRSP"), + } + + account = user_client.post( + "/accounts", + json=account_payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + account_json = account.json() if account.content else {} + + print("Account creation response:", account.status_code, account_json) + + if account.status_code != 200 or "id" not in account_json: + last_failure = f"account={account.status_code} {account.text}" + sleep(500) + continue + + account_id = account_json["id"] + + return { + "token": token, + "account_id": account_id, + } + + except httpx.HTTPError as error: + last_failure = str(error) + print("Setup error:", last_failure) + sleep(500) + + raise RuntimeError(f"Timed out creating authenticated account: {last_failure}") + +def create_transaction(user_client, account_id, token): + transactions = [] + + base_date = datetime(2024, 1, 1) + + for _ in range(12): + # Random amount between 1 and 10000 + amount = round(random.uniform(1, 10000), 2) + + # Random date within 720 days + random_days = random.randint(0, 720) + random_seconds = random.randint(0, 86400) + + transaction_date = base_date + timedelta(days=random_days, seconds=random_seconds) + + transaction_payload = { + "financialAccount_id": account_id, + "amount": amount, + "description": "Sample transaction", + "sender": "Unknown", + "recipient": "Tester", + "date": transaction_date.isoformat(), + "category": "None", + } + + response = user_client.post( + "/transactions", + json=transaction_payload, + headers={"Authorization": f"Bearer {token}"} + ) + + print("Transaction:", response.status_code, response.text) + transactions.append(response.json() if response.content else {}) + +@pytest.fixture(scope="session") +def auth_account(auth_client, user_client) -> Dict: + account = create_authenticated_account( + auth_client=auth_client, + user_client=user_client, + prefix="testuser", + ) + print("Authenticated account created:", account) + create_transaction(user_client, account["account_id"], account["token"]) + return account + diff --git a/app/server/services/python/analytics/integration/test_debts_integration.py b/app/server/services/python/analytics/integration/test_debts_integration.py new file mode 100644 index 0000000..c253fbb --- /dev/null +++ b/app/server/services/python/analytics/integration/test_debts_integration.py @@ -0,0 +1,137 @@ +from typing import Dict + +import httpx +import pytest + +def test_predict_debt_payoff(auth_account: httpx.Client, analytics_client: httpx.Client): + token = auth_account["token"] + account_id = auth_account["account_id"] + + payload = { + "id": str(account_id), + "category": "Credit Card", + "remainingAmount": 2000, + "interestRate": 20, + "minimumPayment": 100, + "period": 30, # days between payments + "nextDueDate": "2024-07-01" + } + + response = analytics_client.post( + "/predict-debt-payoff", + json=payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 200, response.text + + data = response.json() + + assert isinstance(data, dict) + + assert "id" in data + assert "category" in data + assert "minimumPayment" in data + assert "interestRate" in data + assert "debtStages" in data + + assert isinstance(data["debtStages"], list) + assert len(data["debtStages"]) > 0 + + debts = data["debtStages"] + + for debt in debts: + assert "id" in debt + assert "principalAmount" in debt + assert "interestAmount" in debt + assert "remainingDebt" in debt + assert "installmentDate" in debt + +def test_predict_debt_payoff_with_unreasonable_payment(auth_account: httpx.Client, analytics_client: httpx.Client): + token = auth_account["token"] + account_id = auth_account["account_id"] + + # Test with a minimum payment that is too low to ever pay off the debt + payload = { + "id": str(account_id), + "category": "Credit Card", + "remainingAmount": 400000, + "minimumPayment": 100, + "interestRate": 20.0, + "period": 14, # days between payments + "nextDueDate": "2024-07-01" + } + + response = analytics_client.post( + "/predict-debt-payoff", + json=payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 400 + + data = response.json() + print(f"Data: {data}") + + assert isinstance(data, dict) + assert "detail" in data + assert "Minimum payment is too low. Debt will never be paid off." in data["detail"] + +def test_predict_debt_payoff_with_negative_minimum_payment(analytics_client: httpx.Client, auth_account: Dict): + token = auth_account["token"] + account_id = auth_account["account_id"] + + # Test with negative minimum payment + payload = { + "id": str(account_id), + "category": "Credit Card", + "remainingAmount": 400000, + "interestRate": 20.0, + "minimumPayment": -100, + "period": 14, # days between payments + "nextDueDate": "2024-07-01" + } + + response = analytics_client.post( + "/predict-debt-payoff", + json=payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 422, response.text + + data = response.json() + + assert "detail" in data + assert isinstance(data["detail"], list) + assert any("minimumPayment" in str(error) for error in data["detail"]), "Error should mention minimumPayment" + +def test_predict_debt_payoff_with_negative_period(analytics_client: httpx.Client, auth_account: Dict): + token = auth_account["token"] + account_id = auth_account["account_id"] + + # Test with negative period + payload = { + "id": str(account_id), + "category": "Credit Card", + "remainingAmount": 400000, + "interestRate": 20.0, + "minimumPayment": -100, + "period": -14, # days between payments + "nextDueDate": "2024-07-01" + } + + response = analytics_client.post( + "/predict-debt-payoff", + json=payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 422, response.text + + data = response.json() + + assert "detail" in data + assert isinstance(data["detail"], list) + assert any("period" in str(error) for error in data["detail"]), "Error should mention period" + diff --git a/app/server/services/python/analytics/integration/test_savings_integration.py b/app/server/services/python/analytics/integration/test_savings_integration.py new file mode 100644 index 0000000..0f40b5e --- /dev/null +++ b/app/server/services/python/analytics/integration/test_savings_integration.py @@ -0,0 +1,112 @@ +import os +import time +from typing import Dict + +import httpx +import pytest + + +def test_analytics_health_endpoint(analytics_client: httpx.Client) -> None: + response = analytics_client.get("/health") + + assert response.status_code == 200 + assert response.text == "ok" + + print("Analytics service is healthy.") + +def test_compound_interest(analytics_client: httpx.Client, auth_account: Dict) -> None: + token = auth_account["token"] + account_id = auth_account["account_id"] + + payload = { + "financial_account_id": account_id, + "balance": 1000, + "monthly_deposit": 100, + "annual_interest_rate": 5.0, + "time_frame": 2, + } + + response = analytics_client.post( + "/compound-interest", + json=payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 200, response.text + + data = response.json() + + assert isinstance(data, list) + assert len(data) > 0 + + for entry in data: + assert "accumulative_best_balance" in entry + assert "accumulative_expected_balance" in entry + assert "accumulative_worst_balance" in entry + assert "date" in entry + + for entry in data: + best = entry["accumulative_best_balance"] + expected = entry["accumulative_expected_balance"] + worst = entry["accumulative_worst_balance"] + + assert best >= expected >= worst >= 0, "Balances should be non-negative and ordered correctly" + + + first = data[0]["accumulative_expected_balance"] + last = data[-1]["accumulative_expected_balance"] + + assert last >= first, "Balance should grow over time" + +def test_compound_interest_with_negative_monthly_deposit(analytics_client: httpx.Client, auth_account: Dict) -> None: + token = auth_account["token"] + account_id = auth_account["account_id"] + + # Test with negative monthly deposit + payload = { + "financial_account_id": account_id, + "balance": 1000, + "monthly_deposit": -100, + "annual_interest_rate": 5.0, + "time_frame": 2, + } + + response = analytics_client.post( + "/compound-interest", + json=payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 422, response.text + + data = response.json() + assert "detail" in data + assert isinstance(data["detail"], list) + assert any("monthly_deposit" in str(error) for error in data["detail"]), "Error should mention monthly_deposit" + +def test_compound_interest_with_negative_time_frame(analytics_client: httpx.Client, auth_account: Dict) -> None: + token = auth_account["token"] + account_id = auth_account["account_id"] + + # Test with negative time frame + payload = { + "financial_account_id": account_id, + "balance": 1000, + "monthly_deposit": 100, + "annual_interest_rate": 5.0, + "time_frame": -2, + } + + response = analytics_client.post( + "/compound-interest", + json=payload, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 422, response.text + + data = response.json() + assert "detail" in data + assert isinstance(data["detail"], list) + assert any("time_frame" in str(error) for error in data["detail"]), "Error should mention time_frame" + diff --git a/app/server/services/python/analytics/pyproject.toml b/app/server/services/python/analytics/pyproject.toml new file mode 100644 index 0000000..688e46f --- /dev/null +++ b/app/server/services/python/analytics/pyproject.toml @@ -0,0 +1,8 @@ +[tool.mutmut] +paths_to_mutate = ["analytics_src/"] +pytest_add_cli_args_test_selection = ["mutation_test/"] +pytest_add_cli_args = ["-q"] +mutate_only_covered_lines = true +do_not_mutate = ["analytics_src/__init__.py"] +also_copy = ["mutation_test/"] +debug = true diff --git a/app/server/services/python/analytics/requirements-test.txt b/app/server/services/python/analytics/requirements-test.txt index 2236104..04b8062 100644 --- a/app/server/services/python/analytics/requirements-test.txt +++ b/app/server/services/python/analytics/requirements-test.txt @@ -1,3 +1,4 @@ +-r requirements.txt pydantic==2.5.0 pandas==2.1.4 numpy==1.26.3 @@ -7,4 +8,6 @@ pyjwt==2.11.0 pytest>=7.0.0 pytest-asyncio>=0.21.0 pytest-cov>=4.1.0 -pytest-mock>=3.11.0 \ No newline at end of file +pytest-mock>=3.11.0 +httpx>=0.27.0,<0.28.0 +mutmut>=3.2.3 \ No newline at end of file diff --git a/app/server/services/python/analytics/requirements.txt b/app/server/services/python/analytics/requirements.txt index 8b65c9c..1a15f0f 100644 --- a/app/server/services/python/analytics/requirements.txt +++ b/app/server/services/python/analytics/requirements.txt @@ -6,7 +6,7 @@ numpy==1.26.3 mysql-connector-python==8.0.33 dotenv==0.9.9 pyjwt==2.11.0 -pytest>=7.0.0 -pytest-asyncio>=0.21.0 -pytest-cov>=4.1.0 -pytest-mock>=3.11.0 \ No newline at end of file +# pytest>=7.0.0 +# pytest-asyncio>=0.21.0 +# pytest-cov>=4.1.0 +# pytest-mock>=3.11.0 \ No newline at end of file diff --git a/app/server/services/python/analytics/src/logic/debt.py b/app/server/services/python/analytics/src/logic/debt.py index cf763ec..4dafcdf 100644 --- a/app/server/services/python/analytics/src/logic/debt.py +++ b/app/server/services/python/analytics/src/logic/debt.py @@ -9,7 +9,7 @@ def generate_debt_payoff_stages(dbr: DebtPayoffRequest) -> DebtPayoffResponse: minimum_payment = dbr.minimumPayment interest_rate = dbr.interestRate or 0 - monthly_interest_rate = round(interest_rate / 12 / 100, 5) if interest_rate else 0 + monthly_interest_rate = round(interest_rate / 12 / 100, 4) if interest_rate else 0 expected_date = datetime.strptime(dbr.nextDueDate, "%Y-%m-%d") stages: List[DebtPayoffStage] = [] diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py index a57c199..9393c48 100644 --- a/app/server/services/python/analytics/src/main.py +++ b/app/server/services/python/analytics/src/main.py @@ -149,7 +149,7 @@ async def predict_debt_payoff( @app.post('/compound-interest') -async def compount_interest( +async def compound_interest( requestBody: ProjectedSavingsRequest, user_id: int = Depends(get_current_user), ) -> List[CompoundInterestResponse]: diff --git a/app/server/services/python/analytics/src/models/__init__.py b/app/server/services/python/analytics/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/services/python/analytics/src/models/schemas.py b/app/server/services/python/analytics/src/models/schemas.py index be89b8f..26323d6 100644 --- a/app/server/services/python/analytics/src/models/schemas.py +++ b/app/server/services/python/analytics/src/models/schemas.py @@ -1,5 +1,5 @@ from numpy import number -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import List, Literal, Optional from datetime import datetime @@ -58,10 +58,10 @@ class DebtPayoffRequest(BaseModel): id: str category: str remainingAmount: float - minimumPayment: float - interestRate: Optional[float] = 0 + minimumPayment: float = Field(..., gt=0, description="Minimum payment must be greater than 0") + interestRate: Optional[float] = Field(default=0, description="Interest rate in percentage") nextDueDate: str # YYYY-MM-DD - period: int # days + period: int = Field(..., gt=0, description="Period must be greater than 0") # in days class DebtPayoffStage(BaseModel): id: int @@ -80,9 +80,9 @@ class DebtPayoffResponse(BaseModel): class ProjectedSavingsRequest(BaseModel): financial_account_id: int balance: float - monthly_deposit: float - annual_interest_rate: float | None # annual interest rate in percentage - time_frame: int # in years + monthly_deposit: float = Field(..., gt=0, description="Monthly deposit must be greater than 0") + annual_interest_rate: Optional[float] = None # annual interest rate in percentage + time_frame: int = Field(..., gt=0, description="Time frame must be greater than 0") # in years class MonthlySavingGrowthRate(BaseModel): best_case: float expected_case: float diff --git a/app/server/services/python/analytics/test/budgetPerfCalc.py b/app/server/services/python/analytics/test/budgetPerfCalc.py deleted file mode 100644 index dfa3e17..0000000 --- a/app/server/services/python/analytics/test/budgetPerfCalc.py +++ /dev/null @@ -1,152 +0,0 @@ -import pytest -import pandas as pd -from datetime import datetime -from unittest.mock import patch, ANY -from src.logic.budgetPerfCalc import BudgetPerformanceCalculator -from src.models.schemas import BudgetResponse, BudgetCategory - -pytestmark = pytest.mark.asyncio - -class TestBudgetPerformanceCalculator: - - @pytest.fixture - def performance_calc(self): - return BudgetPerformanceCalculator() - - @pytest.fixture - def mock_transactions(self): - return [ - {'category': 'groceries', 'amount': -300, 'date': '2024-03-10'}, - {'category': 'groceries', 'amount': -150, 'date': '2024-03-12'}, - {'category': 'dining', 'amount': -200, 'date': '2024-03-14'}, - {'category': 'entertainment', 'amount': -100, 'date': '2024-03-15'}, - ] - - @pytest.fixture - def sample_budget_response(self): - categories = [ - BudgetCategory( - category='groceries', - type='need', - avg_monthly_spent=300, - monthly_budget=280, - recommended_budget=65, - is_essential=True - ), - BudgetCategory( - category='dining', - type='want', - avg_monthly_spent=200, - monthly_budget=150, - recommended_budget=35, - is_essential=False - ), - BudgetCategory( - category='entertainment', - type='want', - avg_monthly_spent=150, - monthly_budget=120, - recommended_budget=28, - is_essential=False - ), - ] - return BudgetResponse( - budget=categories, - generated_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S') - ) - - # Synchronous tests - def test_get_period_boundaries_weekly(self, performance_calc): - ref_date = pd.Timestamp('2024-03-15') - start, end = performance_calc.get_period_boundaries('w', ref_date) - assert start.strftime('%Y-%m-%d') == '2024-03-10' - assert end.strftime('%Y-%m-%d') == '2024-03-16' - - def test_get_period_boundaries_monthly(self, performance_calc): - ref_date = pd.Timestamp('2024-03-15') - start, end = performance_calc.get_period_boundaries('m', ref_date) - assert start.strftime('%Y-%m-%d') == '2024-03-01' - assert end.strftime('%Y-%m-%d') == '2024-03-31' - - def test_get_period_boundaries_yearly(self, performance_calc): - ref_date = pd.Timestamp('2024-03-15') - start, end = performance_calc.get_period_boundaries('y', ref_date) - assert start.strftime('%Y-%m-%d') == '2024-01-01' - assert end.strftime('%Y-%m-%d') == '2024-12-31' - - def test_calculate_actual_spending(self, performance_calc, mock_transactions): - result = performance_calc.calculate_actual_spending(mock_transactions) - assert result['groceries'] == 450 - assert result['dining'] == 200 - assert result['entertainment'] == 100 - - # Async tests with proper mocking - async def test_generate_performance( - self, - performance_calc, - sample_budget_response, - mock_transactions - ): - user_id = 123 - period = 'w' - - # Mock the database function at the EXACT import path used in budgetPerfCalc - with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: - # Configure the mock to return our test data - mock_get.return_value = mock_transactions - - # Call the async function - result = await performance_calc.generate_performance( - user_id, - sample_budget_response, - period - ) - - # Assertions - assert 'categories' in result - assert 'budgetAmounts' in result - assert 'actualAmounts' in result - assert len(result['categories']) == len(sample_budget_response.budget) - - # Verify the mock was called with correct arguments - mock_get.assert_called_once_with(user_id, ANY, ANY) - - async def test_generate_performance_empty_transactions( - self, - performance_calc, - sample_budget_response - ): - user_id = 123 - period = 'w' - - with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: - mock_get.return_value = [] - - result = await performance_calc.generate_performance( - user_id, - sample_budget_response, - period - ) - - # All actual amounts should be 0 - assert all(amount == 0 for amount in result['actualAmounts']) - mock_get.assert_called_once() - - async def test_generate_performance_db_error( - self, - performance_calc, - sample_budget_response - ): - user_id = 123 - period = 'w' - - with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get: - mock_get.side_effect = Exception("Database connection failed") - - # The error should propagate - with pytest.raises(Exception, match="Database connection failed"): - await performance_calc.generate_performance( - user_id, - sample_budget_response, - period - ) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/budget_integration.py b/app/server/services/python/analytics/test/budget_integration.py deleted file mode 100644 index 56724c3..0000000 --- a/app/server/services/python/analytics/test/budget_integration.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -from unittest.mock import patch, AsyncMock -from src.logic.budget import generate_budget, generate_budget_performance -from src.models.schemas import BudgetResponse - -class TestBudgetIntegration: - - @pytest.mark.asyncio - async def test_full_budget_flow(self): - user_id = 123 - period = 'w' - - # Mock transactions for generate_budget - mock_transactions = [ - {'amount': 5000, 'category': 'salary', 'date': '2024-01-01'}, - {'amount': -300, 'category': 'groceries', 'date': '2024-01-02'}, - {'amount': -200, 'category': 'dining', 'date': '2024-01-03'}, - {'amount': 5000, 'category': 'salary', 'date': '2024-02-01'}, - {'amount': -250, 'category': 'groceries', 'date': '2024-02-02'}, - ] - - with patch( - 'src.logic.budget.get_user_transactions_with_connection', - return_value=mock_transactions - ): - # Generate budget - budget = generate_budget(period, user_id) - - assert isinstance(budget, BudgetResponse) - assert len(budget.budget) > 0 - - # Generate performance - performance = await generate_budget_performance(user_id, budget, period) - - assert 'categories' in performance - assert 'budgetAmounts' in performance - assert 'actualAmounts' in performance - assert len(performance['categories']) == len(budget.budget) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/cat_classifier.py b/app/server/services/python/analytics/test/cat_classifier.py deleted file mode 100644 index ef7cb27..0000000 --- a/app/server/services/python/analytics/test/cat_classifier.py +++ /dev/null @@ -1,55 +0,0 @@ -import pytest -from src.utils.trans_cat_classifier import CategoryClassifier - -class TestCategoryClassifier: - - def test_classify_needs(self, classifier): - assert classifier.classify('rent') == 'need' - assert classifier.classify('groceries') == 'need' - assert classifier.classify('healthcare') == 'need' - assert classifier.classify('utilities') == 'need' - - def test_classify_wants(self, classifier): - assert classifier.classify('dining') == 'want' - assert classifier.classify('entertainment') == 'want' - assert classifier.classify('shopping') == 'want' - assert classifier.classify('coffee') == 'want' - - def test_classify_savings(self, classifier): - assert classifier.classify('savings') == 'savings' - assert classifier.classify('investment') == 'savings' - assert classifier.classify('retirement') == 'savings' - - def test_classify_unknown_defaults_to_want(self, classifier): - assert classifier.classify('unknown_category') == 'want' - assert classifier.classify('random_stuff') == 'want' - - def test_classify_case_insensitive(self, classifier): - assert classifier.classify('RENT') == 'need' - assert classifier.classify('Groceries') == 'need' - assert classifier.classify('DINING') == 'want' - - def test_classify_many(self, classifier): - categories = {'rent', 'dining', 'savings', 'unknown'} - result = classifier.classify_many(categories) - - assert result['rent'] == 'need' - assert result['dining'] == 'want' - assert result['savings'] == 'savings' - assert result['unknown'] == 'want' - - def test_get_categories_by_type(self, classifier): - category_types = { - 'rent': 'need', - 'groceries': 'need', - 'dining': 'want', - 'savings': 'savings' - } - - needs = classifier.get_categories_by_type(category_types, 'need') - wants = classifier.get_categories_by_type(category_types, 'want') - savings = classifier.get_categories_by_type(category_types, 'savings') - - assert needs == {'rent', 'groceries'} - assert wants == {'dining'} - assert savings == {'savings'} \ No newline at end of file diff --git a/app/server/services/python/analytics/test/incomeflow.py b/app/server/services/python/analytics/test/incomeflow.py deleted file mode 100644 index 18945bf..0000000 --- a/app/server/services/python/analytics/test/incomeflow.py +++ /dev/null @@ -1,131 +0,0 @@ -# tests/test_service_incomeflow.py -import pytest -import pandas as pd -from src.logic.incomeflow import build_sankey_data - -class TestIncomeflowService: - - def test_build_sankey_data_with_income_and_expenses(self, mock_incomeflow_transactions): - result = build_sankey_data(mock_incomeflow_transactions) - - assert 'nodes' in result - assert 'links' in result - - # Should have total income node - node_names = [node['name'] for node in result['nodes']] - assert 'total income' in node_names - - # Should have all unique categories as nodes - expected_categories = {'salary', 'freelance', 'interest', 'rent', - 'groceries', 'dining', 'entertainment', 'utilities'} - for category in expected_categories: - assert category in node_names - - # Should have links - assert len(result['links']) > 0 - - # Verify link structure - for link in result['links']: - assert 'source' in link - assert 'target' in link - assert 'value' in link - assert isinstance(link['value'], int) - - def test_build_sankey_data_with_overflow(self): - transactions = [ - {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, - {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, - {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, - {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'}, - ] - - result = build_sankey_data(transactions) - - # Should have 'unspent' node - node_names = [node['name'] for node in result['nodes']] - assert 'unspent' in node_names - - # Should have link to unspent - unspent_index = node_names.index('unspent') - total_income_index = node_names.index('total income') - - # Find link from total income to unspent - unspent_links = [link for link in result['links'] - if link['source'] == total_income_index - and link['target'] == unspent_index] - assert len(unspent_links) == 1 - assert unspent_links[0]['value'] == 4000 # 6000 - 2000 - - def test_build_sankey_data_with_overspending(self): - transactions = [ - {'amount': 3000, 'category': 'salary', 'date': '2024-03-01'}, - {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, - {'amount': -1000, 'category': 'groceries', 'date': '2024-03-04'}, - {'amount': -800, 'category': 'dining', 'date': '2024-03-05'}, - ] - - result = build_sankey_data(transactions) - - # Should have 'overspent' and 'savings' nodes - node_names = [node['name'] for node in result['nodes']] - assert 'overspent' in node_names - assert 'savings' in node_names - - # Should have link from savings to overspent - savings_index = node_names.index('savings') - overspent_index = node_names.index('overspent') - - overspent_links = [link for link in result['links'] - if link['source'] == savings_index - and link['target'] == overspent_index] - assert len(overspent_links) == 1 - assert overspent_links[0]['value'] == 300 # 3300 - 3000 - - def test_build_sankey_data_empty_transactions(self): - """Test sankey data with no transactions.""" - result = build_sankey_data([]) - - assert result['nodes'] == [{'name': 'total income'}, {'name': 'no data'}] - assert result['links'] == [] - - def test_build_sankey_data_only_income(self): - transactions = [ - {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'}, - {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'}, - ] - - result = build_sankey_data(transactions) - - node_names = [node['name'] for node in result['nodes']] - assert 'unspent' in node_names - - # All income should flow to unspent - total_income_index = node_names.index('total income') - unspent_index = node_names.index('unspent') - - unspent_links = [link for link in result['links'] - if link['source'] == total_income_index - and link['target'] == unspent_index] - assert unspent_links[0]['value'] == 6000 - - def test_build_sankey_data_only_expenses(self): - transactions = [ - {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'}, - {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'}, - ] - - result = build_sankey_data(transactions) - - node_names = [node['name'] for node in result['nodes']] - assert 'overspent' in node_names - assert 'savings' in node_names - - # Expenses should come from savings - savings_index = node_names.index('savings') - overspent_index = node_names.index('overspent') - rent_index = node_names.index('rent') - groceries_index = node_names.index('groceries') - - # Links from savings to overspent and to categories - savings_links = [link for link in result['links'] if link['source'] == savings_index] - assert len(savings_links) == 1 # savings into overspent \ No newline at end of file diff --git a/app/server/services/python/analytics/test/periodCalc.py b/app/server/services/python/analytics/test/periodCalc.py deleted file mode 100644 index 8e272e9..0000000 --- a/app/server/services/python/analytics/test/periodCalc.py +++ /dev/null @@ -1,146 +0,0 @@ -import pytest -import pandas as pd -from src.utils.dates import period_calc - -class TestPeriodCalc: - - def test_period_calc_weekly(self): - end_date = pd.Timestamp('2024-03-15') - - start_date, freq, date_format, period_name = period_calc('w', end_date) - - # Check start date is 7 days before end date - expected_start = end_date - pd.Timedelta(days=7) - assert start_date == expected_start - assert start_date.strftime('%Y-%m-%d') == '2024-03-08' - - # Check other return values - assert freq == 'D' - assert date_format == '%Y-%m-%d' - assert period_name == 'Weekly' - - def test_period_calc_monthly(self): - end_date = pd.Timestamp('2024-03-15') - - start_date, freq, date_format, period_name = period_calc('m', end_date) - - # Check start date is 30 days before end date - expected_start = end_date - pd.Timedelta(days=30) - assert start_date == expected_start - assert start_date.strftime('%Y-%m-%d') == '2024-02-14' # March 15 - 30 days = Feb 14 - - # Check other return values - assert freq == 'D' - assert date_format == '%Y-%m-%d' - assert period_name == 'Monthly' - - def test_period_calc_yearly(self): - end_date = pd.Timestamp('2024-03-15') - - start_date, freq, date_format, period_name = period_calc('y', end_date) - - # Check start date is 365 days before end date - expected_start = end_date - pd.Timedelta(days=365) - assert start_date == expected_start - assert start_date.strftime('%Y-%m-%d') == '2023-03-16' # 2024 is leap year - - # Check other return values - assert freq == 'M' - assert date_format == '%Y-%m' - assert period_name == 'Yearly' - - def test_period_calc_with_different_end_dates(self): - test_cases = [ - # (end_date, expected_weekly_start, expected_monthly_start, expected_yearly_start) - ('2024-01-01', '2023-12-25', '2023-12-02', '2023-01-01'), - ('2024-06-15', '2024-06-08', '2024-05-16', '2023-06-16'), - ('2024-12-31', '2024-12-24', '2024-12-01', '2024-01-01'), - ] - - for end_str, expected_weekly, expected_monthly, expected_yearly in test_cases: - end_date = pd.Timestamp(end_str) - - # Test weekly - start, freq, fmt, name = period_calc('w', end_date) - assert start.strftime('%Y-%m-%d') == expected_weekly - - # Test monthly - start, freq, fmt, name = period_calc('m', end_date) - assert start.strftime('%Y-%m-%d') == expected_monthly - - # Test yearly - start, freq, fmt, name = period_calc('y', end_date) - assert start.strftime('%Y-%m-%d') == expected_yearly - - def test_period_calc_edge_cases(self): - - end_date = pd.Timestamp('2024-02-29') # Leap year - start, _, _, _ = period_calc('m', end_date) - assert start.strftime('%Y-%m-%d') == '2024-01-30' # Feb 29 - 30 days = Jan 30 - - end_date = pd.Timestamp('2024-12-31') - start, _, _, _ = period_calc('y', end_date) - assert start.strftime('%Y-%m-%d') == '2024-01-01' - - end_date = pd.Timestamp('2024-01-01') - start, _, _, _ = period_calc('m', end_date) - assert start.strftime('%Y-%m-%d') == '2023-12-02' # Jan 1 - 30 days = Dec 2 - - def test_period_calc_invalid_period(self): - end_date = pd.Timestamp('2024-03-15') - - # Invalid period should go to else branch (yearly) - start, freq, date_format, period_name = period_calc('invalid', end_date) - - expected_start = end_date - pd.Timedelta(days=365) - assert start == expected_start - assert freq == 'M' - assert date_format == '%Y-%m' - assert period_name == 'Yearly' - - def test_period_calc_with_time_component(self): - end_date = pd.Timestamp('2024-03-15 14:30:45') - - start, _, _, _ = period_calc('w', end_date) - - # Time component should be preserved in the calculation - expected_start = end_date - pd.Timedelta(days=7) - assert start == expected_start - assert start.hour == 14 - assert start.minute == 30 - assert start.second == 45 - - def test_period_calc_returns_tuple(self): - end_date = pd.Timestamp('2024-03-15') - result = period_calc('w', end_date) - - assert isinstance(result, tuple) - assert len(result) == 4 - - start, freq, date_format, period_name = result - assert isinstance(start, pd.Timestamp) - assert isinstance(freq, str) - assert isinstance(date_format, str) - assert isinstance(period_name, str) - - @pytest.mark.parametrize("period,expected_freq,expected_format,expected_name", [ - ('w', 'D', '%Y-%m-%d', 'Weekly'), - ('m', 'D', '%Y-%m-%d', 'Monthly'), - ('y', 'M', '%Y-%m', 'Yearly'), - ]) - def test_period_calc_parametrized(self, period, expected_freq, expected_format, expected_name): - end_date = pd.Timestamp('2024-03-15') - - start, freq, date_format, period_name = period_calc(period, end_date) - - assert freq == expected_freq - assert date_format == expected_format - assert period_name == expected_name - - # Verify start date calculation - if period == 'w': - assert start == end_date - pd.Timedelta(days=7) - elif period == 'm': - assert start == end_date - pd.Timedelta(days=30) - else: - assert start == end_date - pd.Timedelta(days=365) \ No newline at end of file diff --git a/app/server/services/python/analytics/test/savings.py b/app/server/services/python/analytics/test/savings.py deleted file mode 100644 index 02d7216..0000000 --- a/app/server/services/python/analytics/test/savings.py +++ /dev/null @@ -1,99 +0,0 @@ -import pytest -import pandas as pd -from datetime import datetime -from unittest.mock import patch, MagicMock -from src.logic.savings import calculate_savings_over_time - -class TestSavings: - - def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_savings_transactions): - end_date = pd.Timestamp('2024-03-15') - start_date = end_date - pd.Timedelta(days=7) - - result = calculate_savings_over_time( - mock_savings_accounts, - mock_savings_transactions, - start_date, - end_date, - 'w', - 'Weekly' - ) - - assert 'labels' in result - assert 'datasets' in result - assert len(result['labels']) == 8 # 7 days + 1? Check your implementation - assert result['datasets'][0]['label'] == 'Weekly Savings' - - # Verify data is numeric - for value in result['datasets'][0]['data']: - assert isinstance(value, (int, float)) - - def test_calculate_savings_over_time_monthly(self, mock_savings_accounts, mock_savings_transactions): - end_date = pd.Timestamp('2024-03-15') - start_date = end_date - pd.Timedelta(days=30) - - result = calculate_savings_over_time( - mock_savings_accounts, - mock_savings_transactions, - start_date, - end_date, - 'm', - 'Monthly' - ) - - assert result['datasets'][0]['label'] == 'Monthly Savings' - assert len(result['labels']) > 0 - - def test_calculate_savings_over_time_yearly(self, mock_savings_accounts, mock_savings_transactions): - end_date = pd.Timestamp('2024-03-15') - start_date = end_date - pd.Timedelta(days=365) - - result = calculate_savings_over_time( - mock_savings_accounts, - mock_savings_transactions, - start_date, - end_date, - 'y', - 'Yearly' - ) - - assert result['datasets'][0]['label'] == 'Yearly Savings' - # Yearly should have monthly labels - assert all(len(label) == 7 for label in result['labels']) # YYYY-MM format - - def test_calculate_savings_over_time_empty_transactions(self, mock_savings_accounts): - end_date = pd.Timestamp('2024-03-15') - start_date = end_date - pd.Timedelta(days=7) - - result = calculate_savings_over_time( - mock_savings_accounts, - [], # Empty transactions - start_date, - end_date, - 'w', - 'Weekly' - ) - - # Should still return structure with zero data - assert 'labels' in result - assert 'datasets' in result - # All savings values should be the initial balances - expected_initial = sum(acc['balance'] for acc in mock_savings_accounts) - assert all(value == expected_initial for value in result['datasets'][0]['data']) - - def test_calculate_savings_over_time_no_accounts(self): - end_date = pd.Timestamp('2024-03-15') - start_date = end_date - pd.Timedelta(days=7) - - result = calculate_savings_over_time( - [], # Empty accounts - [], - start_date, - end_date, - 'w', - 'Weekly' - ) - - # Should return empty structure - assert result['labels'] == [] - assert result['datasets'][0]['data'] == [] \ No newline at end of file diff --git a/app/server/services/python/analytics/test/test_savings.py b/app/server/services/python/analytics/test/test_savings.py index c214666..ccf647f 100644 --- a/app/server/services/python/analytics/test/test_savings.py +++ b/app/server/services/python/analytics/test/test_savings.py @@ -275,7 +275,7 @@ def test_calculate_compound_interest_different_accounts(self): test_cases = [ (5000, 200, 3.5, 2), # High balance, moderate deposits (100, 500, 7.0, 3), # Low balance, high deposits - (10000, 0, 4.0, 1), # High balance, no deposits + (10000, 1, 4.0, 1), # High balance, no deposits (0, 1000, 6.0, 5), # Zero balance, high deposits ] diff --git a/app/server/services/ts/auth/Dockerfile.integration.test b/app/server/services/ts/auth/Dockerfile.integration.test new file mode 100644 index 0000000..97fb570 --- /dev/null +++ b/app/server/services/ts/auth/Dockerfile.integration.test @@ -0,0 +1,10 @@ +FROM measureonecodetwice/finus-auth:local + +COPY auth/integration ./integration + +WORKDIR /project +COPY vitest.config.ts . + +ENV CI=true + +CMD ["bun", "test", "integration", "--coverage"] diff --git a/app/server/services/ts/auth/Dockerfile.mutation.test b/app/server/services/ts/auth/Dockerfile.mutation.test new file mode 100644 index 0000000..46704fd --- /dev/null +++ b/app/server/services/ts/auth/Dockerfile.mutation.test @@ -0,0 +1,15 @@ +FROM measureonecodetwice/finus-auth:local + +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt/lists \ + apt-get update && apt-get install -y nodejs npm procps \ + && rm -rf /var/lib/apt/lists/* + +COPY auth/test ./test + +WORKDIR /project +COPY vitest.config.ts . + +ENV CI=true + +CMD [ "bun", "stryker", "run"] diff --git a/app/server/services/ts/auth/bun.lock b/app/server/services/ts/auth/bun.lock index 5ac2958..6ea5372 100644 --- a/app/server/services/ts/auth/bun.lock +++ b/app/server/services/ts/auth/bun.lock @@ -13,10 +13,15 @@ "devDependencies": { "@types/bun": "^1.3.9", "@types/express": "^5.0.6", + "supertest": "^7.2.2", }, }, }, "packages": { + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], @@ -45,6 +50,10 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -59,6 +68,10 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -67,12 +80,18 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], @@ -87,14 +106,22 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -111,6 +138,8 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], @@ -155,6 +184,10 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -211,6 +244,10 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], + + "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], @@ -222,5 +259,9 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/app/server/services/ts/auth/integration/auth.test.ts b/app/server/services/ts/auth/integration/auth.test.ts new file mode 100644 index 0000000..0823067 --- /dev/null +++ b/app/server/services/ts/auth/integration/auth.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import request from "supertest"; + +const BASE_URL = process.env.API_GATEWAY_ADDR; + +describe("Authentication Integration (Docker)", () => { + let email: string; + let password: string; + const signupEndpoint = "/api/signup"; + const loginEndpoint = "/api/login"; + + beforeAll(() => { + const unique = `auth${Date.now()}${Math.random().toString(36).slice(2, 8)}`; + email = `${unique}@example.com`; + password = "12idontknow"; + }); + + it("signs up a new user", async () => { + const signupBody = { + username: "anything", + email: email, + first_name: "Auth", + last_name: "Test", + age: 40, + password: password, + }; + console.log("Signup body:", signupBody); + const res = await request(BASE_URL).post(signupEndpoint).send(signupBody); + + expect(res.status).toBe(201); + }); + + it("failed signup due to duplicate email", async () => { + const signupBody = { + username: "anything", + email: email, + first_name: "Auth", + last_name: "Test", + age: 40, + password: "12idontknow", + }; + const res = await request(BASE_URL).post(signupEndpoint).send(signupBody); + + expect(res.status).toBe(409); + expect(res.body.error).toBe("An account with this email already exists."); + }); + + it("failed signup due to invalid email format", async () => { + const signupBody = { + username: "anything", + email: "invalidemail", + first_name: "Auth", + last_name: "Test", + age: 40, + password: "12idontknow", + }; + const res = await request(BASE_URL).post(signupEndpoint).send(signupBody); + + expect(res.status).toBe(400); + expect(res.body.error).toBe("invalid email format"); + }); + + it("failed signup due to invalid password", async () => { + const signupBody = { + username: "anything", + email: "newaccount@example.com", + first_name: "Auth", + last_name: "Test", + age: 40, + password: "short", + }; + const res = await request(BASE_URL).post(signupEndpoint).send(signupBody); + + expect(res.status).toBe(400); + expect(res.body.error).toBe( + "Password must be at least 8 characters and include letters and numbers.", + ); + }); + + it("failed signup due to under age", async () => { + const signupBody = { + username: "anything", + email: "newaccount@example.com", + first_name: "Auth", + last_name: "Test", + age: 15, + password: "longenoughpassword1", + }; + const res = await request(BASE_URL).post(signupEndpoint).send(signupBody); + + expect(res.status).toBe(400); + expect(res.body.error).toBe( + "Age must be a whole number between 16 and 120", + ); + }); + + it("failed signup due to over age", async () => { + const signupBody = { + username: "anything", + email: "newaccount@example.com", + first_name: "Auth", + last_name: "Test", + age: 121, + password: "longenoughpassword1", + }; + const res = await request(BASE_URL).post(signupEndpoint).send(signupBody); + + expect(res.status).toBe(400); + expect(res.body.error).toBe( + "Age must be a whole number between 16 and 120", + ); + }); + + it("logs in an existing user", async () => { + const loginBody = { + email: email, + password: password, + }; + const res = await request(BASE_URL).post(loginEndpoint).send(loginBody); + + expect(res.status).toBe(200); + expect(res.body.token).toBeDefined(); + }); + + it("logs in with wrong password", async () => { + const loginBody = { + email: email, + password: "wrongpassword", + }; + const res = await request(BASE_URL).post(loginEndpoint).send(loginBody); + + expect(res.status).toBe(401); + expect(res.body.error).toBe("Invalid email or password."); + }); + + it("logs into non existing account", async () => { + const loginBody = { + email: "nonexist@exmaple.com", + password: "bskdfks312", + }; + const res = await request(BASE_URL).post(loginEndpoint).send(loginBody); + + expect(res.status).toBe(401); + expect(res.body.error).toBe("Invalid email or password."); + }); + + it("logs in with empty credentials", async () => { + const loginBody = { + email: "", + password: "", + }; + const res = await request(BASE_URL).post(loginEndpoint).send(loginBody); + + expect(res.status).toBe(400); + expect(res.body.error).toBe("Missing email"); + }); +}); diff --git a/app/server/services/ts/auth/package.json b/app/server/services/ts/auth/package.json index 0a28cb8..c04db39 100644 --- a/app/server/services/ts/auth/package.json +++ b/app/server/services/ts/auth/package.json @@ -11,10 +11,12 @@ "scripts": { "dev": "bun --watch src/index.ts", "build": "tsc", - "start": "bun dist/index.js" + "start": "bun dist/index.js", + "test": "vitest run" }, "devDependencies": { "@types/bun": "^1.3.9", - "@types/express": "^5.0.6" + "@types/express": "^5.0.6", + "supertest": "^7.2.2" } } diff --git a/app/server/services/ts/auth/stryker.conf.js b/app/server/services/ts/auth/stryker.conf.js new file mode 100644 index 0000000..b2d8c03 --- /dev/null +++ b/app/server/services/ts/auth/stryker.conf.js @@ -0,0 +1,6 @@ +export default { + testRunner: "vitest", + vitest: { + configFile: "vitest.config.ts", + }, +}; diff --git a/app/server/services/ts/auth/vitest.config.ts b/app/server/services/ts/auth/vitest.config.ts new file mode 100644 index 0000000..df9568a --- /dev/null +++ b/app/server/services/ts/auth/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["test/**/*.ts"], + }, +}); diff --git a/app/server/services/ts/bun.lock b/app/server/services/ts/bun.lock index ad93995..7455634 100644 --- a/app/server/services/ts/bun.lock +++ b/app/server/services/ts/bun.lock @@ -21,6 +21,7 @@ "jiti": "^2.6.1", "minimatch": "5", "prettier": "3.8.1", + "supertest": "^7.2.2", "typescript-eslint": "^8.55.0", "vitest": "^4.0.18", }, @@ -224,6 +225,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/8IzqSu4/OWGRs7Fs2ROzGVwJMFTBQkgAp6sAthkBYoN7OiM4rY/CpPVs2X9w9N1W61CHSkEdNKi8HrLZKfK3g=="], "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-TT7eUihnAzxM2tlZesusuC75PAOYKvUBgVU/Nm/lakZ/DpyuqhNkzUfcxSgmmK9IjVWzMmezLIGZl16XGCGJng=="], @@ -248,6 +251,8 @@ "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-cCsXK9AQ9Zf18QlVnbrFu2IKfr4sf2sfbErkF2jfCzyCO9Bnhl0KRx63zlN+Ni1xU7gcBLAssgcui5R400N2eA=="], + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], @@ -372,10 +377,14 @@ "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="], + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -406,8 +415,12 @@ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -418,6 +431,8 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -426,12 +441,16 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -452,6 +471,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -492,6 +513,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], @@ -516,6 +539,10 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -546,6 +573,8 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], @@ -636,6 +665,10 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -778,6 +811,10 @@ "stryker-mutator-bun-runner": ["stryker-mutator-bun-runner@0.4.0", "", { "dependencies": { "@stryker-mutator/api": "^9.0.1", "@stryker-mutator/util": "^9.0.1", "execa": "^9.6.0", "glob": "^11.0.3", "semver": "^7.5.4" }, "peerDependencies": { "@stryker-mutator/core": "^9.0.0", "bun": ">=1.0.0" } }, "sha512-wx2nfdfKFWRxE5fiAYzRBVJoUMtoeYIE2uAtWS1fu4rGeay6XVm3wBmqi2bKosVH6zHPShsciSCDKDGifZHvSA=="], + "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], + + "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -884,6 +921,8 @@ "eslint/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -900,6 +939,8 @@ "eslint/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json index e8d135a..758b166 100644 --- a/app/server/services/ts/package.json +++ b/app/server/services/ts/package.json @@ -13,6 +13,7 @@ "jiti": "^2.6.1", "minimatch": "5", "prettier": "3.8.1", + "supertest": "^7.2.2", "typescript-eslint": "^8.55.0", "vitest": "^4.0.18" }, From a035786851b9f67985c057e03b58da02e28b86f6 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:44:23 -0500 Subject: [PATCH 201/204] Fix frontend build fail in pipeline (#111) --- app/client/Dockerfile | 3 +-- app/client/Dockerfile.test | 2 +- app/client/docker-compose.test.yml | 2 +- app/client/docker-compose.yml | 7 ------- app/client/src/components/TransactionTable.tsx | 1 + app/client/src/pages/LoginPage.tsx | 2 +- 6 files changed, 5 insertions(+), 12 deletions(-) diff --git a/app/client/Dockerfile b/app/client/Dockerfile index 8f0fe98..42e73fa 100644 --- a/app/client/Dockerfile +++ b/app/client/Dockerfile @@ -12,9 +12,8 @@ COPY index.* . COPY public ./public COPY src ./src -FROM finus-webserver-base:local as base RUN bun run build FROM nginx:1.29.5-alpine3.23-perl -COPY --from=base /app/dist /data/www +COPY --from=builder /app/dist /data/www COPY nginx.conf /etc/nginx/nginx.conf diff --git a/app/client/Dockerfile.test b/app/client/Dockerfile.test index 2eb18e8..e7d096f 100644 --- a/app/client/Dockerfile.test +++ b/app/client/Dockerfile.test @@ -1,4 +1,4 @@ -FROM finus-webserver-base:local +FROM local/finus-webserver-base:latest COPY test ./test COPY vitest.config.ts . diff --git a/app/client/docker-compose.test.yml b/app/client/docker-compose.test.yml index 566bead..e54856d 100644 --- a/app/client/docker-compose.test.yml +++ b/app/client/docker-compose.test.yml @@ -1,6 +1,6 @@ services: webserver-base: - image: finus-webserver-base:local + image: local/finus-webserver-base:latest build: dockerfile: Dockerfile.base diff --git a/app/client/docker-compose.yml b/app/client/docker-compose.yml index d1b59e3..9dcbd8b 100644 --- a/app/client/docker-compose.yml +++ b/app/client/docker-compose.yml @@ -1,14 +1,7 @@ services: - webserver-base: - image: $DOCKER_HUB_USERNAME/finus-webserver-base:$RELEASE_VERSION - build: - dockerfile: Dockerfile.base - webserver: image: $DOCKER_HUB_USERNAME/finus-webserver:$RELEASE_VERSION build: dockerfile: Dockerfile ports: - "80:80" - depends_on: - - webserver-base diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx index c68c1b4..c631f80 100644 --- a/app/client/src/components/TransactionTable.tsx +++ b/app/client/src/components/TransactionTable.tsx @@ -11,6 +11,7 @@ import { AiOutlinePlus, AiOutlineClose, AiOutlineUpload, + AiOutlineTransaction, } from "react-icons/ai"; import NoItemState from "./NoItemState.tsx"; import { FiEdit2 } from "react-icons/fi"; diff --git a/app/client/src/pages/LoginPage.tsx b/app/client/src/pages/LoginPage.tsx index ec8d618..92a77b1 100644 --- a/app/client/src/pages/LoginPage.tsx +++ b/app/client/src/pages/LoginPage.tsx @@ -52,7 +52,7 @@ function LoginPage({ onLogin, requestAuth }: LoginPageProps) { Finus - Welcome back + Bye bye Log in to continue managing your finances. From 8d5f47ad5fca151a171b3a89b9d468366e403474 Mon Sep 17 00:00:00 2001 From: Deep Patel Date: Fri, 3 Apr 2026 16:11:05 -0500 Subject: [PATCH 202/204] Added Meeting Minutes and updated Test Plan (#112) --- documentation/Finus_Test_Plan.md | 33 +++++++++++++++---- meeting-minutes/Meeting_11 _March_31.md | 43 +++++++++++++++++++++++++ meeting-minutes/Meeting_12_April_02.md | 43 +++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 meeting-minutes/Meeting_11 _March_31.md create mode 100644 meeting-minutes/Meeting_12_April_02.md diff --git a/documentation/Finus_Test_Plan.md b/documentation/Finus_Test_Plan.md index f15044c..1e317b5 100644 --- a/documentation/Finus_Test_Plan.md +++ b/documentation/Finus_Test_Plan.md @@ -35,15 +35,33 @@ These are the features we have implemented for Sprint 2 and have created tests f 2. Data input 3. Validating forms and files -The following are the features to be implemented in future sprints: Financial Goals, Financial Projections, Stock and FOREX Tracking, Collaborative Budgets (stretch), and ML Integration (stretch). +4\. Financial Goals -The scope includes functional verification through unit, integration, acceptance, and regression testing for the features implemented. +1. Creating, editing, and deleting financial goals +2. Tracking progress for savings and spending reduction goals +3. Budget alignment and recommendations based on selected goals + +5\. Financial Projections + +1. Debt payoff projection logic +2. Savings growth projection logic +3. Validation of projection inputs and calculation requests + +6\. Stock and FOREX Tracking + +1. Searching for stocks and currency pairs +2. Retrieving current quotes and historical market data +3. Pinning and unpinning instruments with dashboard sync on login + +The following features were not able to be implemented and were stretch features if time permitted: Collaborative Budgets, and ML Integration. + +The scope includes functional verification through unit, integration, acceptance, mutation and load testing for the features implemented. The unit and integration tests will confirm code quality and can potentially evaluate some of the project requirements. A major portion of project requirements in our case must be validated through a manual walkthrough within the client. -Load testing will be conducted in Sprint 4 in order to ensure that the server can support the expected usage load, specified in the course outline for the project. +Load testing is conducted in order to ensure that the server can support the expected usage load, specified in the course outline for the project. -Mutation testing will be conducted in Sprint 3 in order to expand code coverage and make the project more robust. +Mutation testing is conducted in order to expand code coverage and make the project more robust. 2. ## **Roles and Responsibilities** @@ -109,8 +127,8 @@ Test levels define the Types of Testing to be executed on the Application Under | Test Level | Scope & Requirement | Methodology (How will you do this?) | | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Unit Testing** | **User Authentication:** 20 tests `
` **User Input:** 30 tests `
` **Visualization Dashboard:** 50 in Python, 46 in TypeScript `
` **Total:** 146 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | -| **Integration Testing** | **10 tests total** covering interactions between features. | *Running tests in their environment with microservices fully operational and communicating with each other. These tests have to be executed locally as CI/CD does not have enough flexibility for this.* | +| **Unit Testing** | **User Authentication:** 20 tests `
` **User Input:** 30 tests `
` **Visualization Dashboard:** 50 in Python, 46 in TypeScript `
` **Stock Tracking:** 83 in Python `
` **User Goals:** 33 tests `
` **User Projections:** 18 tests `
` **Total:** 299 tests | *We use Vitest for TS-based microservices and the client. We use PyTest for python-based microservices.* | +| **Integration Testing** | **User Authentication:** 10 tests `
` **User Input:** 12 tests `
` **Visualization Dashboard:** 1 test `
` **Stock Tracking:** 12 tests `
` **User Goals:** 10 tests `
` **User Projections:** 7 tests `
` **Total:** 52 tests | *Running tests in their environment with microservices fully operational and communicating with each other. These tests have to be executed locally as CI/CD does not have enough flexibility for this.* | | **Acceptance Testing** | **End-user testing** for every user story. | *Team members/external users will perform Manual Walkthroughs based on User Story criteria. Deficiencies and potential improvements will be documented.* | | **Regression Testing** | Unit\+ Integration tests executed on **every push to main branch**. | *We have configured a GitHub Actions CI pipeline to run all tests automatically.* | @@ -155,3 +173,6 @@ Make a mention of any terms or acronyms used in the project | API | Application Program Interface | | AUT | Application Under Test | | JWT | JSON Web Token | +| TS | TypeScript | +| CI | Continuous Integration | +| CD | Continuous Delivery/Deployment| diff --git a/meeting-minutes/Meeting_11 _March_31.md b/meeting-minutes/Meeting_11 _March_31.md new file mode 100644 index 0000000..1222b73 --- /dev/null +++ b/meeting-minutes/Meeting_11 _March_31.md @@ -0,0 +1,43 @@ +Sprint 3 rubric + +* All 6 features completed \- no bugs +* Code deployed to production environment +* Fully working CI/CD pipeline +* Sequence diagrams for every feature +* 100% backend test coverage & 10 *integration* tests per feature +* Acceptance test for each feature + +Sprint 4 rubric + +* Load testing +* Security scanning tool in pipeline +* You need to explicitly mention the coding style and naming convention that you follow. Indicate them in your GitHub page. + +TODO +Code cleanup & standardsation: EVERYONE +Finish Devops \- Logan +Security scan tool \- Logan + +User input unit tests \- Dylan + +Load testing \- Deep + +Acceptance tests: 1 for each user story \- Jackie + +Integration & Mutation tests for analytics & auth \+ projection integration tests \- Huy + +Write down coding conventions \- Roman +User data input UI polish \- Roman +Projections UI polish \- Roman +CSV parsing fix \- Roman + +**Project presentation**: + +- Demo script \- Deep +- Discussion of dev process + - Integration tests w/ docker + - AWS, Terraform + - Bun, Mutation tests + - Multiple development environment issues crlf + +Create presentation slides \ No newline at end of file diff --git a/meeting-minutes/Meeting_12_April_02.md b/meeting-minutes/Meeting_12_April_02.md new file mode 100644 index 0000000..930e8a6 --- /dev/null +++ b/meeting-minutes/Meeting_12_April_02.md @@ -0,0 +1,43 @@ +Sprint 3 rubric + +* All 6 features completed \- no bugs +* Code deployed to production environment +* Fully working CI/CD pipeline +* Sequence diagrams for every feature +* 100% backend test coverage & 10 *integration* tests per feature +* Acceptance test for each feature + +Sprint 4 rubric + +* Load testing +* Security scanning tool in pipeline +* You need to explicitly mention the coding style and naming convention that you follow. Indicate them in your GitHub page. + +TODO +Code cleanup & standardsation: EVERYONE +Finish Devops \- Logan +Security scan tool \- Logan + +User input unit tests \- Dylan + +Load testing \- Deep + +Acceptance tests: 1 for each user story \- Jackie + +Integration & Mutation tests for analytics & auth \+ projection integration tests \- Huy + +~~Write down coding conventions \- Roman~~ +~~User data input UI polish \- Roman~~ +~~Projections UI polish \- Roman~~ +~~CSV parsing fix \- Roman~~ + +**Project presentation**: + +- Demo script \- Deep +- Discussion of dev process + - Integration tests w/ docker + - AWS, Terraform + - Bun, Mutation tests + - Multiple development environment issues crlf + +Create presentation slides \ No newline at end of file From 678a7dd8ab6dd70964e7c09849b098769b76ec18 Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:03:37 -0500 Subject: [PATCH 203/204] Merge main into dev (#114) * add branching strategy diagram * Rename project title in README to 'Finus' * Merge Development Branch into Main * Meeting minutes: added minutes from 2 Feb * Client setup: initialized client skeleton from template with vite * API Gatewat Service: initialized with test api endpoint * Auth Service: initialized service with test api endpoint * User Service: initialized with test api endpoint * Python Services: added the analytics and market services with their initial test api enpoints * API Gateway: fixed test endpoint to also run correctly on docker * API Gateway: added cors to allow Vite's default dev port to access port 3000 (the one that the api service is running on) * Client: setup routing to pages * Client: initialized shadcn ui with tailwindcss * Updated folder structure on client side * Added some draft files to client side * removes -s from service names. Changed the docker compose file a bit to handle ports more dynamically. /api/test -> /health * add database to docker compose * db running w/ sample schema * removed user auth folder * whoops. here's the schema * Client: Added layout, dashboard and header component for setup * finished schema * updated schema * hopefuly final change to special investment type strategy * Client: Created header for dashboard * Defined models for profiles, accounts, and transactions. * Moved model files into a src folder * created Api routes for the db (accounts, transactions, profiles and index) * Created connection.ts. Altered the models to extend "RowDataPacket" * Client: Introduced the first chart * Client: Added cards on dashboard page * Client: Refactored account card to a component * Client: Added second chart on dashboard page * Fixed user auth login and signup to work with microservices * Updated frontend to work with multiple tables in db * Client: Introduced transaction table and animation for cards * reworked project structure to use package & eslint inheritance + commons folder where ts services can share code * fixed/cleaned up git ignore & removed extra READMEs * removed bun.locb * added readme * moved db folder to services * factored out some common methods * factored out some common methods * Client: Polished transaction table and added method to fetch transactions * created bun init files for user input models * Moved index.ts out of routes to be under src * Added Api routes for user input models in api gateways * Server: cleaned up post merge moving dev into visualization, health test works now * DB: expanded account types in schema * Created the popup forms components for account and transactions. * added prettier to format code * formatted server code * git hooks to format pre-commit * removed unused file * formatting AND eslint runs before commit AND removed auth b/c deep is working on it in another branch * pretty client * changed name of hooks folder * Updated User Authentication service to work with updated changes * compose images prepping CI pipeline * moved compose db related env vars to defaults * passing db password via env variable instead of secret * Uploaded forms for csv parsing * Added drop boxes for Account and transition forms * Client: added synthetic data and API skeleton for chart components * initi papaparse for csv reading * Correction on init for papaparse lib * created an utility parser for csv files using papaparse * Utility files for Normalization and validation on csv entry * Implemented a convert row -> transaction draft and updated ParseCsv to use new file instead * Implemented a preview table and updated the upload folder to maintain state * Visualization: enabled synthetic data for API for expenses chart * Updated files to import types * Added confirmation to the csv upload. Also updated Csv Upload to implement this behavior * Implemented transaction backend route for batch csv data upload * Visualization: added savings and income charts with synthetic data * Visualization: updated button component to use default react code * Styled the form components and constricted user input in forms. * re-arranged ts service dockerfiles to better utilize layer caching * added support for .env files in ts services * using http proxy in gateway + env file support * Visualization backend: enabled API endpoints with dummy data * Client: Refactored dashboard chart section and table * Created functions to validate the form data and functions to get/post data to/from the server. * Split login and sign up page into separate files, and reorganized client side files * Finished the api functions for accounts and transactions. * env file update * added publish backend workflow * github workflow checkout: @v4 -> commit SHA * fix CI issues * removed unused password files. Everything is in .env * DB: added a python populator script * Client: rework of the dashboard page to work with updated auth feature * Client: Keep refactoring * Merge: cleaned up post-merge of dev into visualization, populator works at this point * Merge: integreated auth feature to work with the database * Visualization: fixed sankey chart render * Cleanup: got rid of old debugging code * removed empty directories + small ts cleanup * small ts error cleanup * eslint errors, dockerized frontend w/nginx webserver. Increases cachyness of backend dockerfiles & compose * Docker cache improvements to client * hope this uses containerd backend * removed bun.lockb from gitignore. turns out it is good to track them... * now pushing client. will deduplicate maybe * added components and api communications for a income form. * Fixed the url for the fetches and added delete request for income * Fixed api requests * auth tests + prettier * dockerfile changes * Created components to create a list of accounts cards * got auth tests running in dockerfile * Create components to create a list of transactions cards. Transaction form now takes a date of the transaction. * Created components to create a list of user's income with cards. * Cleaned up code, put the object types into their own file. * implemented a csv Success model and updated all relevant forms to reflect this * Temp removed (csv upload from accounts) * refactor workflow image publishing * fixed issue where would check every file if none changed * Added workflow to run tests. Runs on every pull request and pushes to main, dev and release branches * Fixed 404 error, move some of the file around so that user service is handling /accounts, /transactions, and /profiles requests instead of database * update README.md * commit hook bugfix * Added new meeting minutes * Cleaned up code and moved around files so that they'll be ready for the merge into dev branch. * updated README.md * Fixed bug when composing docker for client * improve pre-commit hook script * 100% auth backend coverage! * used prettier to format everything * sequence diagram for auth * get rid of repo layer caching from fontend docker compose * fix cors issue when running frontent via bun * add jwt token to the requests and adjusted user-input files. * Set up to handle user service to handle account requests * Fixed typo for transaction * Sequence Diagram for user input * slight corrections to account router * Refactor api-gateway * Refactored auth frontend to use shadcn ui * Updated app.css * updated to have category field * Added handling of the jwt token but currently is bugged * Test inititialization , started working on tests * fixed unknown type in transaction route * Adjusted types, create types to be used for sql querries and added file ot handle JWT tokens * fat merge of visualization into development * removing parse csv test * post-merge cleanup * Fixed typo in transaction route * Added jwt handling in accounts get request * stabilization for missing data * deleted tests, not valid * post-merge fixes to client * Unit test for normalize rows in csv upload * Fixed small issue with Normalize row * fixed the unit test for normalized row * refactor of user microservice for testing * created unit test for convert transaction * updated the transactions to fit the backend more * Added user flow diagram for dashboard * Unit tests for (validateFile, validate row and parseCsv file(still updating) ) * Finished the rest of the accounts request. * implementation of user logic tests - queries avoided * renaming some files for consistency * got rid of fake timers in user-service tests * Updated current unit tests and added unit tests for validate row and handle input * Fixed accounts get request * refactor of analytics service for testing * Fix the visual of editing an account in account list * Fixed where type was being set when editing an account * Transaction form set it's fields to the edited transaction if we're editing it. * implemented routing for csv and updated delete in the front end for transactions * unit tests for analytics service - visualization dashboard * docker tweaks for tests * uploading unfinished test plan * Minor Fix to where minimum age to sign up for Finus is 16 instead of 1 * Making fixes to transactions components * cleanup and populator rework * fixed parsing logic for csv parsing * Tranasction request handling is fixed * Updated csv route for transactions * updated csv transaction route * sample csv file used for testing * Fixed import for auth session * Fixed a test to allign with new normalizeRow * Updated and Complete Test Plan for Sprint 2 * Fixed minor markdown syntax in test plan * One more minor markdown syntax fix in test plan --------- Co-authored-by: Roman Tebel Co-authored-by: Huy Truong Co-authored-by: Logan Co-authored-by: Dylan Prabagaran Co-authored-by: Jackie Mei --------- Co-authored-by: Deep Patel Co-authored-by: Roman Tebel Co-authored-by: Huy Truong Co-authored-by: Dylan Prabagaran Co-authored-by: Jackie Mei --- app/client/src/assets/react.svg | 1 + app/client/src/components/AccountForm.tsx | 264 ++++++ app/client/src/components/AccountListCard.tsx | 71 ++ app/client/src/components/TransactionCard.tsx | 68 ++ app/client/src/components/TransactionList.tsx | 131 +++ app/client/src/components/TransationForm.tsx | 322 +++++++ app/client/test/validateForms.test.ts | 140 +++ app/server/services/ts/user/bun.lock | 846 ++++++++++++++++++ .../branching-strategy/Branching strategy.svg | 4 + 9 files changed, 1847 insertions(+) create mode 100644 app/client/src/assets/react.svg create mode 100644 app/client/src/components/AccountForm.tsx create mode 100644 app/client/src/components/AccountListCard.tsx create mode 100644 app/client/src/components/TransactionCard.tsx create mode 100644 app/client/src/components/TransactionList.tsx create mode 100644 app/client/src/components/TransationForm.tsx create mode 100644 app/client/test/validateForms.test.ts create mode 100644 app/server/services/ts/user/bun.lock create mode 100644 wiki-resources/branching-strategy/Branching strategy.svg diff --git a/app/client/src/assets/react.svg b/app/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/app/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx new file mode 100644 index 0000000..578df72 --- /dev/null +++ b/app/client/src/components/AccountForm.tsx @@ -0,0 +1,264 @@ +import React, { useState } from "react"; +import "./userForm.css"; +import { postUserAccount, putUserAccount } from "../api/Account.ts"; +import { type Account } from "../types/AccountType.ts"; +import { validateAccountForm } from "../utils/ValidateForms.ts"; +import { + handleCurrencyChange, + handleCurrencyBlur, +} from "../utils/handleInput.ts"; +import { accountCategory } from "@/enum/AccountCategory.ts"; +import type { AuthSession } from "@/types/authTypes.ts"; + +interface popupProp { + toggle: () => void; + session: AuthSession; + setAccount?: (account: Account) => void; + addAccount?: (account: Account) => void; + edit: boolean; + selectedAccount?: Account; +} + +type typeofAccount = keyof typeof accountCategory; + +export default function PopupForm({ + toggle, + session, + setAccount, + addAccount, + edit, + selectedAccount, +}: popupProp) { + //Submits the account data + + const handleSubmit = () => { + let subtype = undefined; + let interest = undefined; + const accountBalance = Number(balance); + + if (accountType === accountCategory.SAVING) { + subtype = formInput.subType; + } + + if ( + accountType === accountCategory.SAVING || + accountType === accountCategory.DEBT + ) { + interest = formInput.interest; + } + + //Check account before validating b/c account can be undefined + //Determine if form input for an account is valid + if ( + accountType && + validateAccountForm( + formInput.name, + accountCategory[accountType], + accountBalance, + undefined, + subtype, + interest, + ) + ) { + const newAccount: Account = { + id: 0, + name: formInput.name, + type: accountType, + balance: accountBalance, + subtype: subtype, + value: 0, + last_updated: new Date(), + }; + console.log(newAccount); + + //Determine if we editing an account info + if (edit && selectedAccount) { + newAccount.id = selectedAccount.id; + + try { + //Put request to update the account + putUserAccount(session, newAccount).then((response) => { + //Determine if sucessfully updated the account + if (response && response.lastUpdated && setAccount) { + newAccount.last_updated = response.lastUpdated; + newAccount.balance = Number(newAccount.balance.toFixed(2)); + setAccount(newAccount); + } + }); + } catch { + alert("Failed to update Account"); + } + } else { + try { + //Send a post request to create + postUserAccount(session, newAccount).then((response) => { + //When creating new account, id and lastupdated is returned for that account + if (response && response.id) { + newAccount.id = response.id; + } + + if (response.lastUpdated) { + newAccount.last_updated = response.lastUpdated; + + //Check if addAcount is undefined + if (addAccount) { + addAccount(newAccount); + } + } + }); + } catch { + alert("Failed to create account " + newAccount.name); + } + } + + toggle(); + } else { + alert("Please fill out the form"); + } + }; + + const handleChange = ( + event: React.ChangeEvent, + ) => { + setFormInput({ ...formInput, [event.target.name]: event.target.value }); + }; + + //Handles the bank statement + /*const handleFile = (event:React.ChangeEvent) => { + + if(event.target && event.target.files && event.target.files[0]){ + setFile(event.target.files[0]) + } else { + setFile(undefined) + } + }*/ + + const [formInput, setFormInput] = useState(() => { + if (edit && selectedAccount) { + return { + name: selectedAccount.name, + subType: selectedAccount.subtype || "", + interest: 0, + }; + } else { + //Default value + return { + name: "", + subType: "", + interest: 0, + }; + } + }); + //const [file, setFile] = useState(undefined) + const [balance, setBalance] = useState(() => { + if (edit && selectedAccount) { + return selectedAccount.balance.toString(); + } else { + //Default value + return ""; + } + }); + + const [accountType, setAccountType] = useState( + () => { + if (edit && selectedAccount) { + return selectedAccount.type as typeofAccount; + } else { + //Default value + return undefined; + } + }, + ); + + const accountCat: typeofAccount[] = Object.keys( + accountCategory, + ) as typeofAccount[]; + + return ( + <> +
+
+ {edit ?

Edit Account

:

Create Account

} + + + +

+ + + +

+ + + handleCurrencyChange(event, setBalance)} + onBlur={(event) => handleCurrencyBlur(event, balance, setBalance)} + placeholder="0.00" + /> +

+ + {accountType === "DEBT" || accountType === "SAVING" ? ( + <> + + +

+ + ) : null} + + {accountType === "SAVING" ? ( + <> + + +

+ + ) : null} + +
+ + {edit ? ( + + ) : ( + + )} +
+
+
+ + ); +} diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx new file mode 100644 index 0000000..4fc5111 --- /dev/null +++ b/app/client/src/components/AccountListCard.tsx @@ -0,0 +1,71 @@ +import { deleteUserAccount } from "../api/Account.ts"; +import { useState } from "react"; +import AccountPopup from "./AccountForm.tsx"; +import { type Account } from "../types/AccountType.ts"; +import type { AuthSession } from "@/types/authTypes.ts"; + +interface cardProp { + account: Account; + session: AuthSession; + setAccount: (editAccount: Account) => void; + removeAccount: (removeAccount: Account) => void; +} + +export default function Card({ + account, + session, + setAccount, + removeAccount, +}: cardProp) { + //Stores the value that toggles thhe account popup form + const [seen, setSeen] = useState(false); + + const toggle = () => { + setSeen(!seen); + }; + + //Deletes the user account from the server and then remove it from the list + const deleteAccount = () => { + try { + //Send a request to delete user account + deleteUserAccount(session, account).then((result) => { + //Sucessfully deleted account + if (result) { + removeAccount(account); + } + }); + } catch { + alert("Failed to delete account " + account.name); + } + }; + + return ( + <> +
+

{account.name}

+

+ +
+

+ {account.type + " " + (account.subtype ? " " + account.type : "")} +

+

{Number(account.balance).toFixed(2)}

+
+ +
+ + +
+
+ {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/TransactionCard.tsx b/app/client/src/components/TransactionCard.tsx new file mode 100644 index 0000000..6401b24 --- /dev/null +++ b/app/client/src/components/TransactionCard.tsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import { deleteTransaction } from "../api/Transaction"; +import { type Transaction } from "../types/Transaction"; +import TransactionPopup from "./TransationForm"; +import type { AuthSession } from "@/types/authTypes"; + +interface cardProp { + transaction: Transaction; + session: AuthSession; + setTransaction: (editedTransaction: Transaction) => void; + removeTransaction: (removeTransaction: Transaction) => void; +} + +export default function Card({ + transaction, + session, + setTransaction, + removeTransaction, +}: cardProp) { + const [seen, setSeen] = useState(false); + + const toggle = () => { + setSeen(!seen); + }; + + //Deletes the user account from the server and then remove it from the list + const deleteTrans = () => { + try { + //Send a request to delete account's transaction + deleteTransaction(session, transaction).then((result) => { + //Sucessfully deleted transaction + if (result) { + removeTransaction(transaction); + } + }); + } catch { + alert("Failed to delete transaction " + transaction.id); + } + }; + + return ( + <> +
+

{transaction.id}

+
+

{transaction.amount + " " + transaction.date}

+

+

{transaction.description}

+
+ +
+ + +
+
+ + {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx new file mode 100644 index 0000000..85deae9 --- /dev/null +++ b/app/client/src/components/TransactionList.tsx @@ -0,0 +1,131 @@ +import { useEffect, useState } from "react"; +import { getTransactions } from "../api/Transaction"; +import { type Transaction } from "../types/Transaction"; +import TransactionPopup from "./TransationForm"; +import TransactionCard from "./TransactionCard"; +import type { AuthSession } from "@/types/authTypes"; +import { type Account } from "@/types/AccountType"; +import { getUserAccounts } from "@/api/Account"; + +interface listProp { + session: AuthSession; +} + +export default function TransactionList({ session }: listProp) { + const [userAccounts, setUserAccounts] = useState([]); + const [selectedAccount, setSelectedAccount] = useState(""); + const [accountTransactions, setAccountTransactions] = useState( + [], + ); + const [seen, setSeen] = useState(false); + + useEffect(() => { + getUserAccounts(session) + .then((accounts) => { + console.log(accounts); + + //Detemrine accounts exist + if (accounts) { + setUserAccounts(accounts); + } else { + //alert("Failed to get accounts"); + } + }) + .catch(() => { + //alert("Failed to get accounts"); + }); + }, [session, selectedAccount]); + + //Try to get the account's transaction from the server + useEffect(() => { + if (selectedAccount) { + getTransactions(session, selectedAccount) + .then((transactions) => { + //Determine if we acquired the accounts transaction + if (transactions) { + setAccountTransactions(transactions); + } + }) + .catch(() => { + alert("Failed to get transaction"); + }); + } + }, [session, selectedAccount]); + + //Adds a account to the list + const addTransaction = (newTransaction: Transaction) => { + setAccountTransactions((userTransactions) => [ + ...userTransactions, + newTransaction, + ]); + }; + + //Removes any transaction from the list thats shares the same id + const removeTransaction = (remove: Transaction) => { + setAccountTransactions((userTransactions) => + userTransactions.filter((transaction) => transaction.id !== remove.id), + ); + }; + + const setTransaction = (editTransaction: Transaction) => { + setAccountTransactions( + accountTransactions.map((transaction) => + transaction.id === editTransaction.id + ? { ...editTransaction } + : transaction, + ), + ); + }; + + const toggle = () => { + setSeen(!seen); + }; + + return ( + <> +
+
+ + +
+ +
+ {accountTransactions.map((transaction) => ( + + ))} +
+ +
+ +
+
+ + {seen ? ( + + ) : null} + + ); +} diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx new file mode 100644 index 0000000..d26ca3a --- /dev/null +++ b/app/client/src/components/TransationForm.tsx @@ -0,0 +1,322 @@ +import { useEffect, useState } from "react"; +import CsvUpload from "./csvread/CsvUpload"; +import "./userForm.css"; +import { getUserAccounts } from "../api/Account"; +import { validateTransactionForm } from "../utils/ValidateForms"; +import { postTranscations, putTranscations } from "../api/Transaction"; +import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput"; +import { type Transaction } from "../types/Transaction"; +import { type Account } from "@/types/AccountType"; +import { transactionCategory } from "@/enum/TransactionCategory"; +import type { AuthSession } from "@/types/authTypes"; + +interface popupProp { + toggle: () => void; + session: AuthSession; + setTransaction?: (editedTransaction: Transaction) => void; + addTransaction?: (newTransaction: Transaction) => void; + edit: boolean; + selectedTransaction?: Transaction; +} + +//Gets the keys of the enum +type typeOfTransaction = keyof typeof transactionCategory; + +//Returns a form of for the user to enter their info +export default function PopupForm({ + toggle, + session, + setTransaction, + addTransaction, + edit, + selectedTransaction, +}: popupProp) { + //Handles the submiting the form + const handleSubmit = () => { + const transferAmount = Number(amount); + let userTransaction: Transaction; + let recipient = ""; + let sender = ""; + + if ( + selectedType && + validateTransactionForm( + selectedAccount, + selectedType, + transferAmount, + selectedDate, + undefined, + ) + ) { + let target; + + if (account) { + if (selectedType === transactionCategory.INCOME) { + target = account.find( + (account) => account.id === selectedAccount, + )?.name; + if (target) { + recipient = target; + } + + sender = other; + } else { + recipient = other; + + target = account.find( + (account) => account.id === selectedAccount, + )?.name; + if (target) { + sender = target; + } + } + } + + userTransaction = { + id: 0, + financialAccount_id: Number(selectedAccount), + recipient: recipient, + sender: sender, + amount: transferAmount, + category: selectedType, + date: new Date(selectedDate), + }; + + if (edit && selectedTransaction) { + try { + //Editing selected transaction + userTransaction.id = selectedTransaction.id; + + //Send a request to update the transaction + putTranscations(session, userTransaction).then((result) => { + //Determine if we're able to able to edit the transaction + if (result && setTransaction) { + setTransaction(userTransaction); + } + }); + } catch { + alert("Failed to edit transaction"); + } + } else { + try { + //Send a request to create the transaction + postTranscations(session, userTransaction).then((response) => { + //Successful put if response is returned + if (response && response.id) { + userTransaction.id = response.id; + + if (addTransaction) { + addTransaction(userTransaction); + } + } + }); + } catch { + alert("Failed to create transaction"); + } + } + + //Need to insert function that sets the state in parent component + + toggle(); + } else { + alert("Please fill out the transaction form"); + } + }; + + //Handle when the user adds a file to the form + /*const handleFile = (event:React.ChangeEvent) =>{ + + if(event.target && event.target.files && event.target.files[0]){ + setFile(event.target.files[0]) + } else { + setFile(undefined) + } + + //Insert cvs parsing function/validation function + }*/ + + //State of the user's account + const [account, setAccount] = useState(); + + useEffect(() => { + getUserAccounts(session) + .then((accounts) => { + console.log(accounts); + //Detemrine accounts exist + if (accounts) { + setAccount(accounts); + } else { + alert( + "Failed to retrieve user's accounts, cannot make a transaction", + ); + //toggle(); + } + }) + .catch(() => { + alert("Failed to retrieve user's accounts, cannot make a transaction"); + //toggle(); + }); + }, [session]); + + //Holds state of user input + const [selectedAccount, setSelectedAccount] = useState(() => { + if (edit && selectedTransaction) { + return selectedTransaction.financialAccount_id; + } else { + return 0; + } + }); + + const [selectedType, setSelectedType] = useState(() => { + if (edit && selectedTransaction) { + return selectedTransaction.category; + } else { + return ""; + } + }); + + const [amount, setAmount] = useState(() => { + if (edit && selectedTransaction) { + return selectedTransaction.amount.toFixed(2); + } else { + return ""; + } + }); + //const [file, setFile] = useState(undefined) + + const [selectedDate, setSelectedDate] = useState(() => { + if (edit && selectedTransaction) { + return new Date(selectedTransaction.date).toISOString().slice(0, 10); + } else { + //Default + return ""; + } + }); + + const [other, setOther] = useState(() => { + if (edit && selectedTransaction) { + if (selectedType === transactionCategory.INCOME) { + return selectedTransaction.sender; + } else { + return selectedTransaction.recipient; + } + } else { + //Default + return ""; + } + }); + + //Holds the types of transfers + const transCat: typeOfTransaction[] = Object.keys( + transactionCategory, + ) as typeOfTransaction[]; + + return ( + <> +
+
+ {edit ?

Edit Transaction

:

Create Transaction

} + + + +

+ + {selectedType == transactionCategory.INCOME ? ( + + ) : ( + + )} + setOther(event.target.value)} + > +

+ + + +

+ + + handleCurrencyChange(event, setAmount)} + onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)} + placeholder="0.00" + /> +

+ + + setSelectedDate(event.target.value)} + /> +

+ + {edit ? null : ( + <> + + {!edit && selectedAccount && ( + { + if (addTransaction) { + newTxs.forEach((tx) => addTransaction(tx)); + } + }} + /> + )} + + )} +

+ +
+ + {edit ? ( + + ) : ( + + )} +
+
+
+ + ); +} diff --git a/app/client/test/validateForms.test.ts b/app/client/test/validateForms.test.ts new file mode 100644 index 0000000..512a0ae --- /dev/null +++ b/app/client/test/validateForms.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect } from "vitest"; +import { + validateAccountForm, + validateTransactionForm, + validateIncomeForm, +} from "../src/utils/ValidateForms"; +import { accountCategory } from "../src/enum/AccountCategory"; +import { transactionCategory } from "../src/enum/TransactionCategory"; +//tests for validateForms functions +function mockFile(): File { + return new File(["dummy"], "test.csv", { type: "text/csv" }); +} + +describe("validateAccountForm", () => { + it("returns true when CSV file is provided (CSV overrides all other checks)", () => { + const file = mockFile(); + const result = validateAccountForm("My Account", "Checking", 100, file); + expect(result).toBe(true); + }); + + it("returns true for valid manual account input", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm("My Account", validType, 500); + expect(result).toBe(true); + }); + + it("returns false when name is missing", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm("", validType, 500); + expect(result).toBe(false); + }); + + it("returns false for invalid account type", () => { + const result = validateAccountForm("My Account", "INVALID_TYPE", 100); + expect(result).toBe(false); + }); + + it("returns false for negative balance", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm("My Account", validType, -10); + expect(result).toBe(false); + }); + + it("returns false for invalid interest rate", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm( + "My Account", + validType, + 100, + undefined, + undefined, + 200, + ); + expect(result).toBe(false); + }); + + it("returns true when optional subtype and interest are valid", () => { + const validType = Object.values(accountCategory)[0]; + + const result = validateAccountForm( + "My Account", + validType, + 100, + undefined, + "sub", + 5, + ); + expect(result).toBe(true); + }); +}); + +describe("validateTransactionForm", () => { + it("returns true when CSV file is provided", () => { + const file = mockFile(); + const result = validateTransactionForm( + 123, + "Deposit", + 100, + "2024-01-01", + file, + ); + expect(result).toBe(true); + }); + + it("returns true for valid manual transaction input", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm(123, validType, 50, "2024-01-01"); + expect(result).toBe(true); + }); + + it("returns false when account_id is missing", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm(0, validType, 50, "2024-01-01"); + expect(result).toBe(false); + }); + + it("returns false for invalid transaction type", () => { + const result = validateTransactionForm( + 123, + "INVALID_TYPE", + 50, + "2024-01-01", + ); + expect(result).toBe(false); + }); + + it("returns false for non-positive amount", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm(123, validType, 0, "2024-01-01"); + expect(result).toBe(false); + }); + + it("returns false when date is missing", () => { + const validType = Object.values(transactionCategory)[0]; + + const result = validateTransactionForm(123, validType, 50, ""); + expect(result).toBe(false); + }); +}); + +describe("validateIncomeForm", () => { + it("returns true for valid name and positive income", () => { + expect(validateIncomeForm("Job", 1000)).toBe(true); + }); + + it("returns false for missing name", () => { + expect(validateIncomeForm("", 1000)).toBe(false); + }); + + it("returns false for non-positive income", () => { + expect(validateIncomeForm("Job", 0)).toBe(false); + }); +}); diff --git a/app/server/services/ts/user/bun.lock b/app/server/services/ts/user/bun.lock new file mode 100644 index 0000000..d88bf07 --- /dev/null +++ b/app/server/services/ts/user/bun.lock @@ -0,0 +1,846 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "user-service", + "dependencies": { + "@mysql/xdevapi": "^8.0.35", + "axios": "^1.13.6", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mysql2": "^3.15.3", + "vitest": "^4.0.18", + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/bun": "latest", + "@types/express": "^5.0.6", + "@types/node": "^25.1.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "globals": "^17.3.0", + "jiti": "^2.6.1", + "typescript-eslint": "^8.54.0", + }, + "peerDependencies": { + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], + + "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@mysql/xdevapi": ["@mysql/xdevapi@8.0.35", "", { "dependencies": { "google-protobuf": "3.19.4", "lossless-json": "2.0.1", "parsimmon": "1.18.1" } }, "sha512-l7HoBt1l0GwdCBXBrqri4kPBbJrqsyvaa4eqXvq50aWWArOTc5XGV06FWF1epxT+RORBy7scZTWFANxpr4Sfuw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "google-protobuf": ["google-protobuf@3.19.4", "", {}, "sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lossless-json": ["lossless-json@2.0.1", "", {}, "sha512-KW/FSL426qblKVvf4ImeMVGr0Je6J9aXvAMUOIU8AzelDj06q47mn6QJ+56lBBd+A8kjrncrxdKQs6ZssAXTmw=="], + + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mysql2": ["mysql2@3.18.2", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw=="], + + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "parsimmon": ["parsimmon@1.18.1", "", {}, "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/wiki-resources/branching-strategy/Branching strategy.svg b/wiki-resources/branching-strategy/Branching strategy.svg new file mode 100644 index 0000000..4ddf4b9 --- /dev/null +++ b/wiki-resources/branching-strategy/Branching strategy.svg @@ -0,0 +1,4 @@ + + + +
Master
Master
Release x.x.1





Releas...
Release x.x.2





Releas...
Feature abc





Featur...
\ No newline at end of file From 33582c3350b637257aae8f397b4fa77ebbc09a3d Mon Sep 17 00:00:00 2001 From: Logan <146045376+MeasureOneCodeTwice@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:03:48 -0500 Subject: [PATCH 204/204] Deployment changes (#115) * Change prod backend & webserver ip addresses & some changes to facilitate that * Change bye bye to Welcome back --- app/client/src/pages/LoginPage.tsx | 2 +- app/client/src/utils/constants.ts | 2 +- app/server/default_container.env | 4 ++++ app/server/services/ts/common/expressUtils.ts | 2 +- deployment/infrastructure/main.tf | 7 ++++++- deployment/infrastructure/modules/finus/main.tf | 4 ++-- deployment/scripts/upgrade-images.sh | 1 + deployment/services/default_container.env | 2 ++ 8 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/client/src/pages/LoginPage.tsx b/app/client/src/pages/LoginPage.tsx index 92a77b1..ec8d618 100644 --- a/app/client/src/pages/LoginPage.tsx +++ b/app/client/src/pages/LoginPage.tsx @@ -52,7 +52,7 @@ function LoginPage({ onLogin, requestAuth }: LoginPageProps) { Finus - Bye bye + Welcome back Log in to continue managing your finances. diff --git a/app/client/src/utils/constants.ts b/app/client/src/utils/constants.ts index bb3cdfa..34b4687 100644 --- a/app/client/src/utils/constants.ts +++ b/app/client/src/utils/constants.ts @@ -1,3 +1,3 @@ -export const BASE_URL = "http://3.150.23.72:3000"; +export const BASE_URL = "http://52.15.117.1:3000"; export const SESSION_STORAGE_KEY = "finus-session"; export const SIDEBAR_WIDTH = 260; diff --git a/app/server/default_container.env b/app/server/default_container.env index 6800694..76451cf 100644 --- a/app/server/default_container.env +++ b/app/server/default_container.env @@ -7,3 +7,7 @@ MARKET_SERVICE_ADDR=http://market:8000 MYSQL_HOST=database MYSQL_PORT=3306 DB_NAME=finus + +#Use the CORS_ALLOWED_ORIGIN environment variable to +#specify an additional (to localhost) non-localhost allowed origin. +CORS_ALLOWED_ORIGIN=http://localhost diff --git a/app/server/services/ts/common/expressUtils.ts b/app/server/services/ts/common/expressUtils.ts index 0385b08..1a56b0d 100644 --- a/app/server/services/ts/common/expressUtils.ts +++ b/app/server/services/ts/common/expressUtils.ts @@ -4,7 +4,7 @@ const defaultMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]; const origins = [ "http://localhost", "http://localhost:8080", - "http://18.190.215.135", //prod webserver + process.env.CORS_ALLOWED_ORIGIN ]; export function buildCorsConfig(opts?: { methods?: string[] }) { diff --git a/deployment/infrastructure/main.tf b/deployment/infrastructure/main.tf index 8a6e583..1d6e8c0 100644 --- a/deployment/infrastructure/main.tf +++ b/deployment/infrastructure/main.tf @@ -104,11 +104,16 @@ resource "aws_route_table" "route_table" { } } +resource "aws_key_pair" "deployer" { + key_name = "prod-key" + public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOX3XB+BgcO5ft0OnL1uq1a3cBdHVieXviFqUgFN7p+m logan@nixos" +} + module "prod" { source = "./modules/finus" environment_name = "prod" - ssh_key_pair_name = "finus-dev" + ssh_key_pair_name = "prod-key" security_group_ids = [aws_security_group.default.id] webserver_security_group_ids = [aws_security_group.webserver.id] backend_security_group_ids = [aws_security_group.services.id] diff --git a/deployment/infrastructure/modules/finus/main.tf b/deployment/infrastructure/modules/finus/main.tf index 52bd19d..26378f7 100644 --- a/deployment/infrastructure/modules/finus/main.tf +++ b/deployment/infrastructure/modules/finus/main.tf @@ -47,7 +47,7 @@ data "aws_ami" "amazon_linux" { resource "aws_instance" "backend" { - instance_type = "t2.nano" + instance_type = "t3.micro" ami = data.aws_ami.amazon_linux.id subnet_id = var.subnet_id key_name = var.ssh_key_pair_name @@ -59,7 +59,7 @@ resource "aws_instance" "backend" { } resource "aws_instance" "webserver" { - instance_type = "t2.nano" + instance_type = "t3.micro" ami = data.aws_ami.amazon_linux.id subnet_id = var.subnet_id key_name = var.ssh_key_pair_name diff --git a/deployment/scripts/upgrade-images.sh b/deployment/scripts/upgrade-images.sh index a96c7ab..aef7680 100755 --- a/deployment/scripts/upgrade-images.sh +++ b/deployment/scripts/upgrade-images.sh @@ -1,3 +1,4 @@ #!/bin/bash sudo docker compose down +sudo docker rmi $(sudo docker images -a -q) sudo docker compose up -d --pull always diff --git a/deployment/services/default_container.env b/deployment/services/default_container.env index ce8659e..e9ca3a5 100644 --- a/deployment/services/default_container.env +++ b/deployment/services/default_container.env @@ -7,3 +7,5 @@ MARKET_SERVICE_ADDR=http://market:8000 MYSQL_HOST=finus-dev-database-1.cluster-cv8mki0o4cg9.us-east-2.rds.amazonaws.com MYSQL_PORT=3306 DB_NAME=finus + +CORS_ALLOWED_ORIGIN=http://3.151.103.140