wip: test write
This commit is contained in:
parent
1cd7941472
commit
3e4bad71c8
16 changed files with 1119 additions and 67 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -8,5 +8,9 @@ node_modules/
|
|||
# Distribution files
|
||||
dist/
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
server/data/
|
||||
|
||||
# User defined
|
||||
memo.md
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import solidPlugin from 'vite-plugin-solid';
|
|||
export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
server: {
|
||||
port: 3001,
|
||||
port: 3002,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
"scripts": {
|
||||
"dev": "concurrently \"pnpm exec tsx watch server/src/index.ts\" \"pnpm -r --filter client run dev\"",
|
||||
"build": "pnpm -r build",
|
||||
"start": "pnpm -r start"
|
||||
"start": "pnpm -r start",
|
||||
"test": "pnpm -r --filter server test"
|
||||
},
|
||||
"keywords": [
|
||||
"llm",
|
||||
|
|
|
|||
441
pnpm-lock.yaml
generated
441
pnpm-lock.yaml
generated
|
|
@ -70,12 +70,21 @@ importers:
|
|||
'@types/node':
|
||||
specifier: ^25.3.3
|
||||
version: 25.3.3
|
||||
'@types/supertest':
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
supertest:
|
||||
specifier: ^7.2.2
|
||||
version: 7.2.2
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@types/node@25.3.3)(tsx@4.21.0)
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -332,6 +341,13 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@noble/hashes@1.8.0':
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
|
||||
cpu: [arm]
|
||||
|
|
@ -475,6 +491,9 @@ packages:
|
|||
peerDependencies:
|
||||
solid-js: ^1.8.6
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
|
|
@ -493,12 +512,21 @@ packages:
|
|||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cookiejar@2.1.5':
|
||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
|
|
@ -511,6 +539,9 @@ packages:
|
|||
'@types/http-errors@2.0.5':
|
||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||
|
||||
'@types/methods@1.1.4':
|
||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||
|
||||
'@types/node@25.3.3':
|
||||
resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==}
|
||||
|
||||
|
|
@ -526,6 +557,41 @@ packages:
|
|||
'@types/serve-static@2.2.0':
|
||||
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
||||
|
||||
'@types/superagent@8.1.9':
|
||||
resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==}
|
||||
|
||||
'@types/supertest@7.2.0':
|
||||
resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==}
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
|
||||
|
||||
'@vitest/mocker@4.0.18':
|
||||
resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0-0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
|
||||
|
||||
'@vitest/runner@4.0.18':
|
||||
resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
|
||||
|
||||
'@vitest/snapshot@4.0.18':
|
||||
resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
|
||||
|
||||
'@vitest/spy@4.0.18':
|
||||
resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
|
||||
|
||||
'@vitest/utils@4.0.18':
|
||||
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -538,6 +604,16 @@ packages:
|
|||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
asap@2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
babel-plugin-jsx-dom-expressions@0.40.5:
|
||||
resolution: {integrity: sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg==}
|
||||
peerDependencies:
|
||||
|
|
@ -597,6 +673,10 @@ packages:
|
|||
caniuse-lite@1.0.30001776:
|
||||
resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -615,6 +695,13 @@ packages:
|
|||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
component-emitter@1.3.1:
|
||||
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
||||
|
||||
concurrently@9.2.1:
|
||||
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -639,6 +726,9 @@ packages:
|
|||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cookiejar@2.1.4:
|
||||
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
|
||||
|
||||
cors@2.8.6:
|
||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
|
@ -667,6 +757,10 @@ packages:
|
|||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -675,6 +769,9 @@ packages:
|
|||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dezalgo@1.0.4:
|
||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||
|
||||
dotenv@17.3.1:
|
||||
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -711,10 +808,17 @@ packages:
|
|||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-module-lexer@1.7.0:
|
||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -727,6 +831,9 @@ packages:
|
|||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
etag@1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -735,10 +842,17 @@ packages:
|
|||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
expect-type@1.3.0:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
express@5.2.1:
|
||||
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
fast-safe-stringify@2.1.1:
|
||||
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
@ -759,10 +873,18 @@ packages:
|
|||
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
formidable@3.5.4:
|
||||
resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -816,6 +938,10 @@ packages:
|
|||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -871,6 +997,9 @@ packages:
|
|||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -887,14 +1016,31 @@ packages:
|
|||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
methods@1.1.2:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@3.0.2:
|
||||
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mime@2.6.0:
|
||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
hasBin: true
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -944,6 +1090,9 @@ packages:
|
|||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
obug@2.1.1:
|
||||
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||
|
||||
on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -961,6 +1110,9 @@ packages:
|
|||
path-to-regexp@8.3.0:
|
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
|
|
@ -1080,6 +1232,9 @@ packages:
|
|||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
|
||||
|
|
@ -1098,10 +1253,16 @@ packages:
|
|||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1117,6 +1278,14 @@ packages:
|
|||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
superagent@10.3.0:
|
||||
resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
supertest@7.2.2:
|
||||
resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1132,10 +1301,21 @@ packages:
|
|||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyexec@1.0.2:
|
||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyrainbow@3.0.3:
|
||||
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
|
@ -1242,10 +1422,49 @@ packages:
|
|||
vite:
|
||||
optional: true
|
||||
|
||||
vitest@4.0.18:
|
||||
resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
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: '*'
|
||||
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
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -1481,6 +1700,12 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
optional: true
|
||||
|
||||
|
|
@ -1560,6 +1785,8 @@ snapshots:
|
|||
dependencies:
|
||||
solid-js: 1.9.11
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
|
|
@ -1590,14 +1817,23 @@ snapshots:
|
|||
'@types/connect': 3.4.38
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
assertion-error: 2.0.1
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/cookiejar@2.1.5': {}
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/express-serve-static-core@5.1.1':
|
||||
|
|
@ -1615,6 +1851,8 @@ snapshots:
|
|||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
'@types/methods@1.1.4': {}
|
||||
|
||||
'@types/node@25.3.3':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
|
@ -1632,6 +1870,57 @@ snapshots:
|
|||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/superagent@8.1.9':
|
||||
dependencies:
|
||||
'@types/cookiejar': 2.1.5
|
||||
'@types/methods': 1.1.4
|
||||
'@types/node': 25.3.3
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/supertest@7.2.0':
|
||||
dependencies:
|
||||
'@types/methods': 1.1.4
|
||||
'@types/superagent': 8.1.9
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.0.18
|
||||
'@vitest/utils': 4.0.18
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.3)(tsx@4.21.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.18
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.3.3)(tsx@4.21.0)
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
dependencies:
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/runner@4.0.18':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.0.18
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.0.18':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@4.0.18': {}
|
||||
|
||||
'@vitest/utils@4.0.18':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.2
|
||||
|
|
@ -1643,6 +1932,12 @@ snapshots:
|
|||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
asap@2.0.6: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
babel-plugin-jsx-dom-expressions@0.40.5(@babel/core@7.29.0):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
|
|
@ -1719,6 +2014,8 @@ snapshots:
|
|||
|
||||
caniuse-lite@1.0.30001776: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
|
|
@ -1738,6 +2035,12 @@ snapshots:
|
|||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
component-emitter@1.3.1: {}
|
||||
|
||||
concurrently@9.2.1:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
|
|
@ -1757,6 +2060,8 @@ snapshots:
|
|||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
cookiejar@2.1.4: {}
|
||||
|
||||
cors@2.8.6:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
|
|
@ -1776,10 +2081,17 @@ snapshots:
|
|||
|
||||
deep-extend@0.6.0: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
dezalgo@1.0.4:
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
wrappy: 1.0.2
|
||||
|
||||
dotenv@17.3.1: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
|
|
@ -1806,10 +2118,19 @@ snapshots:
|
|||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
|
|
@ -1843,10 +2164,16 @@ snapshots:
|
|||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
express@5.2.1:
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
|
|
@ -1880,6 +2207,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
fast-safe-stringify@2.1.1: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
|
@ -1902,10 +2231,24 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
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.35
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
formidable@3.5.4:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2': 2.3.1
|
||||
dezalgo: 1.0.4
|
||||
once: 1.4.0
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
|
@ -1951,6 +2294,10 @@ snapshots:
|
|||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
|
@ -1993,6 +2340,10 @@ snapshots:
|
|||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
|
@ -2003,12 +2354,22 @@ snapshots:
|
|||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
methods@1.1.2: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mime-types@3.0.2:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
|
||||
mime@2.6.0: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
|
@ -2041,6 +2402,8 @@ snapshots:
|
|||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
obug@2.1.1: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
|
@ -2057,6 +2420,8 @@ snapshots:
|
|||
|
||||
path-to-regexp@8.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
|
@ -2238,6 +2603,8 @@ snapshots:
|
|||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
|
||||
simple-get@4.0.1:
|
||||
|
|
@ -2263,8 +2630,12 @@ snapshots:
|
|||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
|
|
@ -2281,6 +2652,28 @@ snapshots:
|
|||
|
||||
strip-json-comments@2.0.1: {}
|
||||
|
||||
superagent@10.3.0:
|
||||
dependencies:
|
||||
component-emitter: 1.3.1
|
||||
cookiejar: 2.1.4
|
||||
debug: 4.4.3
|
||||
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.15.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
supertest@7.2.2:
|
||||
dependencies:
|
||||
cookie-signature: 1.2.2
|
||||
methods: 1.1.2
|
||||
superagent: 10.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
|
@ -2304,11 +2697,17 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tinyrainbow@3.0.3: {}
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
|
@ -2378,8 +2777,50 @@ snapshots:
|
|||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.3.3)(tsx@4.21.0)
|
||||
|
||||
vitest@4.0.18(@types/node@25.3.3)(tsx@4.21.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.18
|
||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.3)(tsx@4.21.0))
|
||||
'@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.3.0
|
||||
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: 7.3.1(@types/node@25.3.3)(tsx@4.21.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.3.3
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
@ -25,7 +27,10 @@
|
|||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.3.3",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,49 @@ import fs from 'fs';
|
|||
|
||||
const ANALYTICS_DB_PATH = process.env.ANALYTICS_DB_PATH || path.join(process.cwd(), 'data', 'analytics.db');
|
||||
|
||||
// Ensure data directory exists
|
||||
const dataDir = path.dirname(ANALYTICS_DB_PATH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
let db: Database.Database;
|
||||
|
||||
export function getAnalyticsDb(): Database.Database {
|
||||
if (!db) {
|
||||
const dataDir = path.dirname(ANALYTICS_DB_PATH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(ANALYTICS_DB_PATH);
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
const schemaPath = path.join(__dirname, '..', '..', '..', 'database', 'analytics-schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
db.exec(schema);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
const db = new Database(ANALYTICS_DB_PATH);
|
||||
export function initAnalyticsDb(): Database.Database {
|
||||
// Close existing connection if any
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
|
||||
const dataDir = path.dirname(ANALYTICS_DB_PATH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Enable foreign keys
|
||||
db.pragma('foreign_keys = ON');
|
||||
db = new Database(ANALYTICS_DB_PATH);
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
// Initialize schema
|
||||
const schemaPath = path.join(__dirname, '..', '..', '..', 'database', 'analytics-schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
db.exec(schema);
|
||||
const schemaPath = path.join(__dirname, '..', '..', '..', 'database', 'analytics-schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
db.exec(schema);
|
||||
|
||||
export default db;
|
||||
return db;
|
||||
}
|
||||
|
||||
export function closeAnalyticsDb(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = undefined as unknown as Database.Database;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,49 @@ import fs from 'fs';
|
|||
|
||||
const CORE_DB_PATH = process.env.CORE_DB_PATH || path.join(process.cwd(), 'data', 'core.db');
|
||||
|
||||
// Ensure data directory exists
|
||||
const dataDir = path.dirname(CORE_DB_PATH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
let db: Database.Database;
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (!db) {
|
||||
const dataDir = path.dirname(CORE_DB_PATH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(CORE_DB_PATH);
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
const schemaPath = path.join(__dirname, '..', '..', '..', 'database', 'schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
db.exec(schema);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
const db = new Database(CORE_DB_PATH);
|
||||
export function initDb(): Database.Database {
|
||||
// Close existing connection if any
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
|
||||
const dataDir = path.dirname(CORE_DB_PATH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Enable foreign keys
|
||||
db.pragma('foreign_keys = ON');
|
||||
db = new Database(CORE_DB_PATH);
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
// Initialize schema
|
||||
const schemaPath = path.join(__dirname, '..', '..', '..', 'database', 'schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
db.exec(schema);
|
||||
const schemaPath = path.join(__dirname, '..', '..', '..', 'database', 'schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
db.exec(schema);
|
||||
|
||||
export default db;
|
||||
return db;
|
||||
}
|
||||
|
||||
export function closeDb(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = undefined as unknown as Database.Database;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import db from '../config/database';
|
||||
import { getDb } from '../config/database';
|
||||
import { Backend, CreateBackendData, UpdateBackendData } from '../../../shared/types';
|
||||
|
||||
export class BackendModel {
|
||||
static findAll(): Backend[] {
|
||||
return db.prepare('SELECT * FROM backends ORDER BY created_at DESC').all() as Backend[];
|
||||
return getDb().prepare('SELECT * FROM backends ORDER BY created_at DESC').all() as Backend[];
|
||||
}
|
||||
|
||||
static findById(id: number): Backend | undefined {
|
||||
return db.prepare('SELECT * FROM backends WHERE id = ?').get(id) as Backend | undefined;
|
||||
return getDb().prepare('SELECT * FROM backends WHERE id = ?').get(id) as Backend | undefined;
|
||||
}
|
||||
|
||||
static findActive(): Backend[] {
|
||||
return db.prepare('SELECT * FROM backends WHERE is_active = 1 ORDER BY name').all() as Backend[];
|
||||
return getDb().prepare('SELECT * FROM backends WHERE is_active = 1 ORDER BY name').all() as Backend[];
|
||||
}
|
||||
|
||||
static create(data: CreateBackendData): Backend {
|
||||
const stmt = db.prepare(
|
||||
const stmt = getDb().prepare(
|
||||
'INSERT INTO backends (name, base_url, api_key) VALUES (?, ?, ?)'
|
||||
);
|
||||
const result = stmt.run(data.name, data.base_url, data.api_key || null);
|
||||
|
|
@ -49,7 +49,7 @@ export class BackendModel {
|
|||
}
|
||||
if (data.is_active !== undefined) {
|
||||
updates.push('is_active = ?');
|
||||
values.push(data.is_active);
|
||||
values.push(data.is_active ? 1 : 0);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
|
|
@ -59,17 +59,17 @@ export class BackendModel {
|
|||
updates.push('updated_at = CURRENT_TIMESTAMP');
|
||||
values.push(id);
|
||||
|
||||
db.prepare(`UPDATE backends SET ${updates.join(', ')} WHERE id = ?`).run(...values);
|
||||
getDb().prepare(`UPDATE backends SET ${updates.join(', ')} WHERE id = ?`).run(...values);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
static delete(id: number): boolean {
|
||||
const result = db.prepare('DELETE FROM backends WHERE id = ?').run(id);
|
||||
const result = getDb().prepare('DELETE FROM backends WHERE id = ?').run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static deactivate(id: number): boolean {
|
||||
const result = db.prepare('UPDATE backends SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
|
||||
const result = getDb().prepare('UPDATE backends SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import db from '../config/database';
|
||||
import { getDb } from '../config/database';
|
||||
import { Permission, CreatePermissionData } from '../../../shared/types';
|
||||
|
||||
export class PermissionModel {
|
||||
static findAll(): Permission[] {
|
||||
return db.prepare('SELECT * FROM permissions ORDER BY created_at DESC').all() as Permission[];
|
||||
return getDb().prepare('SELECT * FROM permissions ORDER BY created_at DESC').all() as Permission[];
|
||||
}
|
||||
|
||||
static findByUserId(userId: number): Permission[] {
|
||||
return db.prepare('SELECT * FROM permissions WHERE user_id = ? ORDER BY backend_id').all(userId) as Permission[];
|
||||
return getDb().prepare('SELECT * FROM permissions WHERE user_id = ? ORDER BY backend_id').all(userId) as Permission[];
|
||||
}
|
||||
|
||||
static findByBackendId(backendId: number): Permission[] {
|
||||
return db.prepare('SELECT * FROM permissions WHERE backend_id = ? ORDER BY user_id').all(backendId) as Permission[];
|
||||
return getDb().prepare('SELECT * FROM permissions WHERE backend_id = ? ORDER BY user_id').all(backendId) as Permission[];
|
||||
}
|
||||
|
||||
static findUserBackendPermissions(userId: number, backendId: number): Permission | undefined {
|
||||
return db.prepare('SELECT * FROM permissions WHERE user_id = ? AND backend_id = ?').get(userId, backendId) as Permission | undefined;
|
||||
return getDb().prepare('SELECT * FROM permissions WHERE user_id = ? AND backend_id = ?').get(userId, backendId) as Permission | undefined;
|
||||
}
|
||||
|
||||
static create(data: CreatePermissionData): Permission {
|
||||
try {
|
||||
const stmt = db.prepare(
|
||||
const stmt = getDb().prepare(
|
||||
'INSERT INTO permissions (user_id, backend_id) VALUES (?, ?)'
|
||||
);
|
||||
const result = stmt.run(data.user_id, data.backend_id);
|
||||
|
|
@ -40,22 +40,22 @@ export class PermissionModel {
|
|||
}
|
||||
|
||||
static delete(user_id: number, backend_id: number): boolean {
|
||||
const result = db.prepare('DELETE FROM permissions WHERE user_id = ? AND backend_id = ?').run(user_id, backend_id);
|
||||
const result = getDb().prepare('DELETE FROM permissions WHERE user_id = ? AND backend_id = ?').run(user_id, backend_id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static deleteByUserId(userId: number): number {
|
||||
const result = db.prepare('DELETE FROM permissions WHERE user_id = ?').run(userId);
|
||||
const result = getDb().prepare('DELETE FROM permissions WHERE user_id = ?').run(userId);
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
static deleteByBackendId(backendId: number): number {
|
||||
const result = db.prepare('DELETE FROM permissions WHERE backend_id = ?').run(backendId);
|
||||
const result = getDb().prepare('DELETE FROM permissions WHERE backend_id = ?').run(backendId);
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
static getUserBackendIds(userId: number): number[] {
|
||||
const rows = db.prepare('SELECT backend_id FROM permissions WHERE user_id = ?').all(userId) as { backend_id: number }[];
|
||||
const rows = getDb().prepare('SELECT backend_id FROM permissions WHERE user_id = ?').all(userId) as { backend_id: number }[];
|
||||
return rows.map(row => row.backend_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
import db from '../config/database';
|
||||
import { getDb } from '../config/database';
|
||||
import { User, CreateUserData, UpdateUserData } from '../../../shared/types';
|
||||
|
||||
export class UserModel {
|
||||
static findAll(): User[] {
|
||||
return db.prepare('SELECT * FROM users ORDER BY created_at DESC').all() as User[];
|
||||
return getDb().prepare('SELECT * FROM users ORDER BY created_at DESC').all() as User[];
|
||||
}
|
||||
|
||||
static findById(id: number): User | undefined {
|
||||
return db.prepare('SELECT * FROM users WHERE id = ?').get(id) as User | undefined;
|
||||
return getDb().prepare('SELECT * FROM users WHERE id = ?').get(id) as User | undefined;
|
||||
}
|
||||
|
||||
static findByApiKey(apiKey: string): User | undefined {
|
||||
return db.prepare('SELECT * FROM users WHERE api_key = ? AND is_active = 1').get(apiKey) as User | undefined;
|
||||
return getDb().prepare('SELECT * FROM users WHERE api_key = ? AND is_active = 1').get(apiKey) as User | undefined;
|
||||
}
|
||||
|
||||
static create(data: CreateUserData): User {
|
||||
const stmt = db.prepare(
|
||||
const apiKey = `sk-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
const stmt = getDb().prepare(
|
||||
'INSERT INTO users (api_key, name, email) VALUES (?, ?, ?)'
|
||||
);
|
||||
const result = stmt.run(data.name, data.email || null);
|
||||
const result = stmt.run(apiKey, data.name, data.email || null);
|
||||
|
||||
return {
|
||||
id: result.lastInsertRowid as number,
|
||||
api_key: data.name,
|
||||
api_key: apiKey,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
is_active: true,
|
||||
|
|
@ -55,17 +56,17 @@ export class UserModel {
|
|||
updates.push('updated_at = CURRENT_TIMESTAMP');
|
||||
values.push(id);
|
||||
|
||||
db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...values);
|
||||
getDb().prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...values);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
static delete(id: number): boolean {
|
||||
const result = db.prepare('DELETE FROM users WHERE id = ?').run(id);
|
||||
const result = getDb().prepare('DELETE FROM users WHERE id = ?').run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static deactivate(id: number): boolean {
|
||||
const result = db.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
|
||||
const result = getDb().prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +75,7 @@ export class UserModel {
|
|||
if (!user) return null;
|
||||
|
||||
const newApiKey = `sk-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
db.prepare('UPDATE users SET api_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(newApiKey, id);
|
||||
getDb().prepare('UPDATE users SET api_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(newApiKey, id);
|
||||
return newApiKey;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import analyticsDb from '../config/analytics-db';
|
||||
import { getAnalyticsDb } from '../config/analytics-db';
|
||||
import { RequestLog } from '../../../shared/types';
|
||||
|
||||
export class AnalyticsService {
|
||||
static logRequest(logData: Omit<RequestLog, 'id' | 'created_at'>): void {
|
||||
const stmt = analyticsDb.prepare(`
|
||||
const db = getAnalyticsDb();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO request_logs (
|
||||
user_id, backend_id, endpoint, request_model, response_model,
|
||||
prompt_tokens, completion_tokens, total_tokens,
|
||||
|
|
@ -30,9 +31,10 @@ export class AnalyticsService {
|
|||
}
|
||||
|
||||
private static updateUsageStats(userId: number, backendId: number, tokens: number): void {
|
||||
const db = getAnalyticsDb();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const upsertStmt = analyticsDb.prepare(`
|
||||
const upsertStmt = db.prepare(`
|
||||
INSERT INTO usage_stats (user_id, backend_id, date, total_requests, total_tokens)
|
||||
VALUES (?, ?, ?, 1, ?)
|
||||
ON CONFLICT(user_id, backend_id, date)
|
||||
|
|
@ -45,10 +47,11 @@ export class AnalyticsService {
|
|||
}
|
||||
|
||||
private static updateBackendMetrics(backendId: number, logData: Omit<RequestLog, 'id' | 'created_at'>): void {
|
||||
const db = getAnalyticsDb();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const isSuccess = logData.status_code >= 200 && logData.status_code < 300;
|
||||
|
||||
const existing = analyticsDb.prepare(
|
||||
const existing = db.prepare(
|
||||
'SELECT * FROM backend_metrics WHERE backend_id = ? AND date = ?'
|
||||
).get(backendId, today) as {
|
||||
total_requests: number;
|
||||
|
|
@ -66,7 +69,7 @@ export class AnalyticsService {
|
|||
: existing.avg_response_time_ms;
|
||||
const newSuccessRate = (newTotalRequests - newErrorCount) / newTotalRequests;
|
||||
|
||||
analyticsDb.prepare(`
|
||||
db.prepare(`
|
||||
UPDATE backend_metrics SET
|
||||
total_requests = ?,
|
||||
total_tokens = ?,
|
||||
|
|
@ -76,7 +79,7 @@ export class AnalyticsService {
|
|||
WHERE backend_id = ? AND date = ?
|
||||
`).run(newTotalRequests, newTotalTokens, newAvgResponseTime, newErrorCount, newSuccessRate, backendId, today);
|
||||
} else {
|
||||
analyticsDb.prepare(`
|
||||
db.prepare(`
|
||||
INSERT INTO backend_metrics (
|
||||
backend_id, date, total_requests, total_tokens,
|
||||
avg_response_time_ms, error_count, success_rate
|
||||
|
|
@ -93,12 +96,14 @@ export class AnalyticsService {
|
|||
}
|
||||
|
||||
static getRequestLogs(limit: number = 100, offset: number = 0): RequestLog[] {
|
||||
return analyticsDb.prepare(`
|
||||
const db = getAnalyticsDb();
|
||||
return db.prepare(`
|
||||
SELECT * FROM request_logs ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||
`).all(limit, offset) as RequestLog[];
|
||||
}
|
||||
|
||||
static getUsageStats(userId?: number, backendId?: number, days: number = 30): unknown[] {
|
||||
const db = getAnalyticsDb();
|
||||
const endDate = new Date().toISOString().split('T')[0];
|
||||
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
|
|
@ -119,10 +124,11 @@ export class AnalyticsService {
|
|||
|
||||
query += ' ORDER BY date DESC, user_id, backend_id';
|
||||
|
||||
return analyticsDb.prepare(query).all(...params);
|
||||
return db.prepare(query).all(...params);
|
||||
}
|
||||
|
||||
static getBackendMetrics(backendId?: number, days: number = 30): unknown[] {
|
||||
const db = getAnalyticsDb();
|
||||
const endDate = new Date().toISOString().split('T')[0];
|
||||
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
|
|
@ -139,6 +145,6 @@ export class AnalyticsService {
|
|||
|
||||
query += ' ORDER BY date DESC';
|
||||
|
||||
return analyticsDb.prepare(query).all(...params);
|
||||
return db.prepare(query).all(...params);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
328
server/tests/integration/admin.test.ts
Normal file
328
server/tests/integration/admin.test.ts
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import { createTestApp } from '../utils/testApp';
|
||||
|
||||
let app: ReturnType<typeof createTestApp>;
|
||||
|
||||
beforeAll(() => {
|
||||
app = createTestApp();
|
||||
});
|
||||
|
||||
describe('Admin API - User Management', () => {
|
||||
describe('GET /admin/users', () => {
|
||||
it('should return empty array initially', async () => {
|
||||
const response = await request(app).get('/admin/users');
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /admin/users', () => {
|
||||
it('should create a new user', async () => {
|
||||
const userData = { name: 'Test User', email: 'test@example.com' };
|
||||
const response = await request(app).post('/admin/users').send(userData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.name).toBe(userData.name);
|
||||
expect(response.body.email).toBe(userData.email);
|
||||
expect(response.body).toHaveProperty('api_key');
|
||||
expect(response.body.api_key).toMatch(/^sk-/);
|
||||
});
|
||||
|
||||
it('should return 400 if name is missing', async () => {
|
||||
const response = await request(app).post('/admin/users').send({ email: 'test@example.com' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /admin/users/:id', () => {
|
||||
let userId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app).post('/admin/users').send({ name: 'User for Get' });
|
||||
userId = response.body.id;
|
||||
});
|
||||
|
||||
it('should return a user by id', async () => {
|
||||
const response = await request(app).get(`/admin/users/${userId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.id).toBe(userId);
|
||||
expect(response.body).toHaveProperty('api_key');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent user', async () => {
|
||||
const response = await request(app).get('/admin/users/99999');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /admin/users/:id', () => {
|
||||
let userId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app).post('/admin/users').send({ name: 'User for Update' });
|
||||
userId = response.body.id;
|
||||
});
|
||||
|
||||
it('should update user', async () => {
|
||||
const response = await request(app)
|
||||
.put(`/admin/users/${userId}`)
|
||||
.send({ name: 'Updated Name', email: 'updated@example.com' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.name).toBe('Updated Name');
|
||||
expect(response.body.email).toBe('updated@example.com');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent user', async () => {
|
||||
const response = await request(app).put('/admin/users/99999').send({ name: 'Test' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /admin/users/:id/regenerate-api-key', () => {
|
||||
let userId: number;
|
||||
let oldApiKey: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app).post('/admin/users').send({ name: 'User for Key Regen' });
|
||||
userId = response.body.id;
|
||||
oldApiKey = response.body.api_key;
|
||||
});
|
||||
|
||||
it('should regenerate API key', async () => {
|
||||
const response = await request(app).post(`/admin/users/${userId}/regenerate-api-key`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.api_key).toMatch(/^sk-/);
|
||||
expect(response.body.api_key).not.toBe(oldApiKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /admin/users/:id', () => {
|
||||
let userId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app).post('/admin/users').send({ name: 'User for Delete' });
|
||||
userId = response.body.id;
|
||||
});
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const response = await request(app).delete(`/admin/users/${userId}`);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
});
|
||||
|
||||
it('should return 404 for already deleted user', async () => {
|
||||
const response = await request(app).delete(`/admin/users/${userId}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Admin API - Backend Management', () => {
|
||||
describe('GET /admin/backends', () => {
|
||||
it('should return empty array initially', async () => {
|
||||
const response = await request(app).get('/admin/backends');
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /admin/backends', () => {
|
||||
it('should create a new backend', async () => {
|
||||
const backendData = {
|
||||
name: 'Test Backend',
|
||||
base_url: 'http://localhost:8000/v1',
|
||||
api_key: 'backend-key-123'
|
||||
};
|
||||
const response = await request(app).post('/admin/backends').send(backendData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.name).toBe(backendData.name);
|
||||
expect(response.body.base_url).toBe(backendData.base_url);
|
||||
expect(response.body.api_key).toBe(backendData.api_key);
|
||||
});
|
||||
|
||||
it('should return 400 if name or base_url is missing', async () => {
|
||||
const response = await request(app).post('/admin/backends').send({ name: 'Test' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /admin/backends/:id', () => {
|
||||
let backendId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app).post('/admin/backends').send({
|
||||
name: 'Backend for Get',
|
||||
base_url: 'http://localhost:8001/v1'
|
||||
});
|
||||
backendId = response.body.id;
|
||||
});
|
||||
|
||||
it('should return a backend by id', async () => {
|
||||
const response = await request(app).get(`/admin/backends/${backendId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.id).toBe(backendId);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent backend', async () => {
|
||||
const response = await request(app).get('/admin/backends/99999');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /admin/backends/:id', () => {
|
||||
let backendId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app).post('/admin/backends').send({
|
||||
name: 'Backend for Update',
|
||||
base_url: 'http://localhost:8002/v1'
|
||||
});
|
||||
backendId = response.body.id;
|
||||
});
|
||||
|
||||
it('should update backend', async () => {
|
||||
const response = await request(app)
|
||||
.put(`/admin/backends/${backendId}`)
|
||||
.send({ name: 'Updated Backend', is_active: false });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.name).toBe('Updated Backend');
|
||||
expect(response.body.is_active).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent backend', async () => {
|
||||
const response = await request(app).put('/admin/backends/99999').send({ name: 'Test' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /admin/backends/:id', () => {
|
||||
let backendId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app).post('/admin/backends').send({
|
||||
name: 'Backend for Delete',
|
||||
base_url: 'http://localhost:8003/v1'
|
||||
});
|
||||
backendId = response.body.id;
|
||||
});
|
||||
|
||||
it('should delete a backend', async () => {
|
||||
const response = await request(app).delete(`/admin/backends/${backendId}`);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
});
|
||||
|
||||
it('should return 404 for already deleted backend', async () => {
|
||||
const response = await request(app).delete(`/admin/backends/${backendId}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Admin API - Permission Management', () => {
|
||||
let userId: number;
|
||||
let backendId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const userResponse = await request(app).post('/admin/users').send({ name: 'User for Permission' });
|
||||
userId = userResponse.body.id;
|
||||
|
||||
const backendResponse = await request(app).post('/admin/backends').send({
|
||||
name: 'Backend for Permission',
|
||||
base_url: 'http://localhost:8004/v1'
|
||||
});
|
||||
backendId = backendResponse.body.id;
|
||||
});
|
||||
|
||||
describe('GET /admin/permissions', () => {
|
||||
it('should return empty array initially', async () => {
|
||||
const response = await request(app).get('/admin/permissions');
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /admin/permissions', () => {
|
||||
it('should create a new permission', async () => {
|
||||
const response = await request(app)
|
||||
.post('/admin/permissions')
|
||||
.send({ user_id: userId, backend_id: backendId });
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.user_id).toBe(userId);
|
||||
expect(response.body.backend_id).toBe(backendId);
|
||||
});
|
||||
|
||||
it('should return 409 if permission already exists', async () => {
|
||||
const response = await request(app)
|
||||
.post('/admin/permissions')
|
||||
.send({ user_id: userId, backend_id: backendId });
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
|
||||
it('should return 400 if user_id or backend_id is missing', async () => {
|
||||
const response = await request(app).post('/admin/permissions').send({ user_id: userId });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /admin/permissions/user/:userId', () => {
|
||||
it('should return permissions for user', async () => {
|
||||
const response = await request(app).get(`/admin/permissions/user/${userId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /admin/permissions/backend/:backendId', () => {
|
||||
it('should return permissions for backend', async () => {
|
||||
const response = await request(app).get(`/admin/permissions/backend/${backendId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /admin/permissions', () => {
|
||||
it('should delete a permission', async () => {
|
||||
const response = await request(app)
|
||||
.delete(`/admin/permissions?user_id=${userId}&backend_id=${backendId}`);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
});
|
||||
|
||||
it('should return 404 for already deleted permission', async () => {
|
||||
const response = await request(app)
|
||||
.delete(`/admin/permissions?user_id=${userId}&backend_id=${backendId}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
121
server/tests/integration/api.test.ts
Normal file
121
server/tests/integration/api.test.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import { createTestApp } from '../utils/testApp';
|
||||
import { initDb } from '../../src/config/database';
|
||||
|
||||
describe('Auth & Proxy API', () => {
|
||||
let app: ReturnType<typeof createTestApp>;
|
||||
let userApiKey: string;
|
||||
let backendId: number;
|
||||
|
||||
beforeAll(() => {
|
||||
// Ensure DB is initialized
|
||||
initDb();
|
||||
app = createTestApp();
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a user
|
||||
const userResponse = await request(app).post('/admin/users').send({ name: 'Test User for API' });
|
||||
userApiKey = userResponse.body.api_key;
|
||||
|
||||
// Create a backend
|
||||
const backendResponse = await request(app).post('/admin/backends').send({
|
||||
name: 'Backend for API Test',
|
||||
base_url: 'http://localhost:8005/v1'
|
||||
});
|
||||
backendId = backendResponse.body.id;
|
||||
|
||||
// Grant permission
|
||||
await request(app)
|
||||
.post('/admin/permissions')
|
||||
.send({ user_id: userResponse.body.id, backend_id: backendId });
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('should return health status', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('status', 'ok');
|
||||
expect(response.body).toHaveProperty('timestamp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /v1/chat/completions without auth', () => {
|
||||
it('should return 401 without API key', async () => {
|
||||
const response = await request(app)
|
||||
.post('/v1/chat/completions')
|
||||
.send({ model: 'test', messages: [] });
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
|
||||
it('should return 401 with invalid API key', async () => {
|
||||
const response = await request(app)
|
||||
.post('/v1/chat/completions')
|
||||
.set('Authorization', 'Bearer invalid-key')
|
||||
.send({ model: 'test', messages: [] });
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /v1/chat/completions with valid auth', () => {
|
||||
it('should return 502 when backend is unreachable (but auth passes)', async () => {
|
||||
const response = await request(app)
|
||||
.post('/v1/chat/completions')
|
||||
.set('Authorization', `Bearer ${userApiKey}`)
|
||||
.send({
|
||||
model: 'test-model',
|
||||
messages: [{ role: 'user', content: 'Hello' }]
|
||||
});
|
||||
|
||||
// Should authenticate successfully but fail to connect to backend
|
||||
expect(response.status).toBe(502);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v1/models without permission', () => {
|
||||
it('should return 403 for user without backend permission', async () => {
|
||||
// Create a user without permissions
|
||||
const userResponse = await request(app).post('/admin/users').send({ name: 'User Without Permission' });
|
||||
const invalidApiKey = userResponse.body.api_key;
|
||||
|
||||
const response = await request(app)
|
||||
.get('/v1/models')
|
||||
.set('Authorization', `Bearer ${invalidApiKey}`);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Analytics Logging', () => {
|
||||
it('should log requests to analytics', async () => {
|
||||
// Make a request that will fail (backend unreachable) but should be logged
|
||||
await request(app)
|
||||
.post('/v1/chat/completions')
|
||||
.set('Authorization', `Bearer ${userApiKey}`)
|
||||
.send({
|
||||
model: 'test-model',
|
||||
messages: [{ role: 'user', content: 'Test message' }]
|
||||
});
|
||||
|
||||
// Check analytics
|
||||
const analyticsResponse = await request(app).get('/admin/analytics/requests?limit=10');
|
||||
|
||||
expect(analyticsResponse.status).toBe(200);
|
||||
expect(Array.isArray(analyticsResponse.body)).toBe(true);
|
||||
|
||||
// Find our logged request
|
||||
const loggedRequest = analyticsResponse.body.find((r: any) =>
|
||||
r.status_code === 502 && r.endpoint === '/v1/chat/completions'
|
||||
);
|
||||
|
||||
expect(loggedRequest).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
33
server/tests/setup.ts
Normal file
33
server/tests/setup.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { beforeAll, afterAll } from 'vitest';
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
// Test database paths
|
||||
const TEST_CORE_DB_PATH = path.join(__dirname, '..', 'data', 'test-core.db');
|
||||
const TEST_ANALYTICS_DB_PATH = path.join(__dirname, '..', 'data', 'test-analytics.db');
|
||||
|
||||
// Set environment variables for test databases
|
||||
process.env.CORE_DB_PATH = TEST_CORE_DB_PATH;
|
||||
process.env.ANALYTICS_DB_PATH = TEST_ANALYTICS_DB_PATH;
|
||||
|
||||
// Clear test databases before all tests
|
||||
beforeAll(() => {
|
||||
try {
|
||||
execSync(`rm -f "${TEST_CORE_DB_PATH}" "${TEST_ANALYTICS_DB_PATH}"`, { stdio: 'ignore' });
|
||||
} catch (e) {
|
||||
// Ignore errors if files don't exist
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up after all tests
|
||||
afterAll(() => {
|
||||
import('../src/config/database').then(({ closeDb }) => closeDb()).catch(() => {});
|
||||
import('../src/config/analytics-db').then(({ closeAnalyticsDb }) => closeAnalyticsDb()).catch(() => {});
|
||||
|
||||
// Remove test databases
|
||||
try {
|
||||
execSync(`rm -f "${TEST_CORE_DB_PATH}" "${TEST_ANALYTICS_DB_PATH}"`, { stdio: 'ignore' });
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
});
|
||||
38
server/tests/utils/testApp.ts
Normal file
38
server/tests/utils/testApp.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { initDb } from '../../src/config/database';
|
||||
import { initAnalyticsDb } from '../../src/config/analytics-db';
|
||||
import adminRoutes from '../../src/routes/admin';
|
||||
import apiRoutes from '../../src/routes/api';
|
||||
import analyticsRoutes from '../../src/routes/analytics';
|
||||
|
||||
export function createTestApp() {
|
||||
// Initialize both databases
|
||||
initDb();
|
||||
initAnalyticsDb();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/admin', adminRoutes);
|
||||
app.use('/v1', apiRoutes);
|
||||
app.use('/admin/analytics', analyticsRoutes);
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error('Error:', err.message);
|
||||
res.status(500).json({ error: err.message || 'Internal server error' });
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Not found' });
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
16
server/vitest.config.ts
Normal file
16
server/vitest.config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
testTimeout: 10000,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue