wip: test write

This commit is contained in:
Kyush 2026-03-06 02:06:39 +09:00
commit 3e4bad71c8
16 changed files with 1119 additions and 67 deletions

4
.gitignore vendored
View file

@ -8,5 +8,9 @@ node_modules/
# Distribution files
dist/
# Database files
*.db
server/data/
# User defined
memo.md

View file

@ -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',

View file

@ -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
View file

@ -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

View file

@ -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"
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View 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);
});
});
});

View 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
View 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
}
});

View 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
View 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'),
},
},
});