From a59c26e8f17d45ec079b48606c5ef6e9a56dcd63 Mon Sep 17 00:00:00 2001 From: Shadow-MMN Date: Sun, 28 Jun 2026 17:41:43 +0100 Subject: [PATCH 1/4] feat: add pagination, streamId, and since filters to GET /api/events - Add ?pageSize, ?streamId, and ?since query parameters - Updated getGlobalEvents and countAllEvents to support all filters - Updated listEventsQuerySchema with new optional fields - Changed default pagination to use pageSize=20 instead of returning all - Added Swagger documentation for the endpoint - Added integration tests covering each filter independently and combined Closes #380 --- backend/package-lock.json | 749 +++++++++++++++++++++++++++ backend/src/index.ts | 21 +- backend/src/integration.test.ts | 75 +++ backend/src/services/eventHistory.ts | 75 ++- backend/src/swagger.ts | 93 ++++ backend/src/validation/schemas.ts | 12 + 6 files changed, 997 insertions(+), 28 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 0100229..097b1be 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -230,6 +230,74 @@ "node": ">=12" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", "cpu": [ @@ -245,6 +313,312 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "dev": true, @@ -595,6 +969,34 @@ "version": "0.4.0", "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.60.4", "cpu": [ @@ -607,6 +1009,353 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "hasInstallScript": true, diff --git a/backend/src/index.ts b/backend/src/index.ts index 7f22196..61eca60 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -23,6 +23,7 @@ import { getStreamHistory, countStreamEvents, getStreamEventSummary, + StreamEventType, } from "./services/eventHistory"; import { fetchOpenIssues } from "./services/openIssues"; import { @@ -588,25 +589,27 @@ app.get("/api/events", readLimiter, (req: Request, res: Response) => { } const query = parsedQuery.data; - const hasPage = req.query.page !== undefined; - const hasLimit = req.query.limit !== undefined; - const eventType = query.eventType as Parameters[2]; - const total = countAllEvents(eventType); + const eventType = query.eventType as StreamEventType | undefined; + const streamId = query.streamId; + const since = query.since; + + const total = countAllEvents(eventType, streamId, since); const page = query.page ?? PAGINATION_DEFAULT_PAGE; - const limit = - !hasPage && !hasLimit ? total : (query.limit ?? PAGINATION_DEFAULT_LIMIT); + const pageSize = query.pageSize ?? query.limit ?? PAGINATION_DEFAULT_LIMIT; - const offset = (page - 1) * limit; + const offset = (page - 1) * pageSize; const data = getGlobalEvents( - limit === 0 ? 0 : limit, + pageSize, offset, eventType, query.cursor, + streamId, + since, ); - res.json({ data, total, page, limit }); + res.json({ data, total, page, pageSize, limit: pageSize }); }); app.get( diff --git a/backend/src/integration.test.ts b/backend/src/integration.test.ts index 3641732..3c0453a 100644 --- a/backend/src/integration.test.ts +++ b/backend/src/integration.test.ts @@ -1608,6 +1608,81 @@ describe("Backend Integration Tests", () => { expect(streamIds.has("2")).toBe(true); expect(streamIds.has("3")).toBe(true); }); + + it("should filter by streamId", async () => { + const response = await request(app) + .get("/api/events") + .query({ streamId: "1" }); + + expect(response.status).toBe(200); + expect(response.body.data).toHaveLength(2); + expect(response.body.total).toBe(2); + response.body.data.forEach((e: any) => { + expect(e.streamId).toBe("1"); + }); + }); + + it("should filter by since timestamp", async () => { + const now = Math.floor(Date.now() / 1000); + // Events were inserted at now+1, now+2, now+3 (created) and now+100 (canceled) + // Filtering since now+50 should only return the canceled event + const response = await request(app) + .get("/api/events") + .query({ since: now + 50 }); + + expect(response.status).toBe(200); + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0].eventType).toBe("canceled"); + }); + + it("should use pageSize parameter (default 20)", async () => { + const response = await request(app) + .get("/api/events") + .query({ pageSize: 2 }); + + expect(response.status).toBe(200); + expect(response.body.data).toHaveLength(2); + expect(response.body.pageSize).toBe(2); + expect(response.body.total).toBe(4); + expect(response.body.page).toBe(1); + }); + + it("should combine eventType and streamId filters", async () => { + const response = await request(app) + .get("/api/events") + .query({ eventType: "created", streamId: "1" }); + + expect(response.status).toBe(200); + expect(response.body.data).toHaveLength(1); + expect(response.body.total).toBe(1); + expect(response.body.data[0].eventType).toBe("created"); + expect(response.body.data[0].streamId).toBe("1"); + }); + + it("should combine eventType, streamId, and since filters", async () => { + const now = Math.floor(Date.now() / 1000); + const response = await request(app) + .get("/api/events") + .query({ eventType: "created", streamId: "1", since: now }); + + expect(response.status).toBe(200); + expect(response.body.data).toHaveLength(1); + expect(response.body.total).toBe(1); + expect(response.body.data[0].eventType).toBe("created"); + expect(response.body.data[0].streamId).toBe("1"); + }); + + it("should paginate with pageSize", async () => { + const response = await request(app) + .get("/api/events") + .query({ page: 1, pageSize: 2 }); + + expect(response.status).toBe(200); + expect(response.body.data).toHaveLength(2); + expect(response.body.total).toBe(4); + expect(response.body.page).toBe(1); + expect(response.body.pageSize).toBe(2); + }); }); }); diff --git a/backend/src/services/eventHistory.ts b/backend/src/services/eventHistory.ts index 363d7ac..0dc429b 100644 --- a/backend/src/services/eventHistory.ts +++ b/backend/src/services/eventHistory.ts @@ -112,37 +112,74 @@ export function getGlobalEvents( offset: number, eventType?: StreamEventType, cursor?: number, + streamId?: string, + since?: number, ): StreamEvent[] { const db = getDb(); + const conditions: string[] = []; + const params: any[] = []; + if (eventType) { - let query = `SELECT * FROM stream_events WHERE event_type = ?`; - const params: any[] = [eventType]; + conditions.push("event_type = ?"); + params.push(eventType); + } + + if (cursor !== undefined) { + conditions.push("id < ?"); + params.push(cursor); + } - if (cursor !== undefined) { - query += ` AND id < ?`; - params.push(cursor); - } + if (streamId) { + conditions.push("stream_id = ?"); + params.push(streamId); + } - query += ` ORDER BY timestamp DESC, id DESC LIMIT ? OFFSET ?`; - params.push(limit, offset); + if (since !== undefined) { + conditions.push("timestamp > ?"); + params.push(since); + } - const rows = db.prepare(query).all(...params) as EventRow[]; - return rows.map(rowToEvent); + let query = "SELECT * FROM stream_events"; + if (conditions.length > 0) { + query += " WHERE " + conditions.join(" AND "); } - return getAllEvents(limit, offset, cursor); + query += " ORDER BY timestamp DESC, id DESC LIMIT ? OFFSET ?"; + params.push(limit, offset); + + const rows = db.prepare(query).all(...params) as EventRow[]; + return rows.map(rowToEvent); } -export function countAllEvents(eventType?: StreamEventType): number { +export function countAllEvents( + eventType?: StreamEventType, + streamId?: string, + since?: number, +): number { const db = getDb(); + const conditions: string[] = []; + const params: any[] = []; + if (eventType) { - const row = db - .prepare(`SELECT COUNT(*) as count FROM stream_events WHERE event_type = ?`) - .get(eventType) as { count: number }; - return row.count; + conditions.push("event_type = ?"); + params.push(eventType); } - const row = db - .prepare(`SELECT COUNT(*) as count FROM stream_events`) - .get() as { count: number }; + + if (streamId) { + conditions.push("stream_id = ?"); + params.push(streamId); + } + + if (since !== undefined) { + conditions.push("timestamp > ?"); + params.push(since); + } + + let query = "SELECT COUNT(*) as count FROM stream_events"; + if (conditions.length > 0) { + query += " WHERE " + conditions.join(" AND "); + } + + const row = db.prepare(query).get(...params) as { count: number }; return row.count; } diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts index 616be68..21cab2a 100644 --- a/backend/src/swagger.ts +++ b/backend/src/swagger.ts @@ -1635,6 +1635,99 @@ export const swaggerDocument = { }, }, }, + "/api/events": { + get: { + summary: "List All Events", + description: + "Retrieves all stream events across all streams with pagination and optional filtering by event type, stream ID, and timestamp.", + parameters: [ + { + name: "page", + in: "query", + required: false, + description: "Page number (default: 1).", + schema: { type: "integer", minimum: 1 }, + }, + { + name: "pageSize", + in: "query", + required: false, + description: "Number of events per page (default: 20, max: 100).", + schema: { type: "integer", minimum: 1, maximum: 100 }, + }, + { + name: "limit", + in: "query", + required: false, + description: "Alias for pageSize. If both are provided, pageSize takes precedence.", + schema: { type: "integer", minimum: 1, maximum: 100 }, + }, + { + name: "eventType", + in: "query", + required: false, + description: "Filter by event type: created, claimed, canceled, paused, resumed, start_time_updated.", + schema: { + type: "string", + enum: ["created", "claimed", "canceled", "paused", "resumed", "start_time_updated"], + }, + }, + { + name: "streamId", + in: "query", + required: false, + description: "Filter by stream ID.", + schema: { type: "string" }, + }, + { + name: "since", + in: "query", + required: false, + description: "Filter to events after this unix timestamp (in seconds).", + schema: { type: "integer" }, + }, + { + name: "cursor", + in: "query", + required: false, + description: "Cursor for cursor-based pagination (event ID).", + schema: { type: "integer" }, + }, + ], + responses: { + "200": { + description: "Paginated list of events.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + data: { + type: "array", + items: { + $ref: "#/components/schemas/StreamEvent", + }, + }, + total: { + type: "integer", + description: "Total number of events matching the filters.", + }, + page: { + type: "integer", + description: "Current page number.", + }, + pageSize: { + type: "integer", + description: "Number of events per page.", + }, + }, + }, + }, + }, + }, + }, + }, + }, "/api/open-issues": { get: { summary: "Get Open Issues", diff --git a/backend/src/validation/schemas.ts b/backend/src/validation/schemas.ts index 1fd5dd2..1b292eb 100644 --- a/backend/src/validation/schemas.ts +++ b/backend/src/validation/schemas.ts @@ -163,6 +163,18 @@ export const listEventsQuerySchema = z.object({ .min(1, "limit must be greater than or equal to 1") .max(100, "limit must be less than or equal to 100") .optional(), + pageSize: z + .coerce.number() + .int("pageSize must be an integer") + .min(1, "pageSize must be greater than or equal to 1") + .max(100, "pageSize must be less than or equal to 100") + .optional(), + streamId: z.string().trim().optional(), + since: z + .coerce.number() + .int("since must be an integer") + .positive("since must be a positive unix timestamp") + .optional(), cursor: z .coerce.number() .int("cursor must be an integer") From dc32371c1b736338c0a6506f20424aa98efc5b0a Mon Sep 17 00:00:00 2001 From: Shadow-MMN Date: Sun, 28 Jun 2026 18:00:00 +0100 Subject: [PATCH 2/4] fix: add completed to eventType enum and reject blank streamId in events schema - Add 'completed' to VALID_EVENT_TYPES in schemas.ts and the swagger eventType enum to match the handler's accepted StreamEventType values - Add 400 error response to /api/events swagger definition - Reject blank streamId values (e.g. streamId=) at the schema boundary while keeping the field optional when omitted entirely --- backend/src/swagger.ts | 14 ++++++++++++-- backend/src/validation/schemas.ts | 8 ++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts index 21cab2a..65f3e25 100644 --- a/backend/src/swagger.ts +++ b/backend/src/swagger.ts @@ -1666,10 +1666,10 @@ export const swaggerDocument = { name: "eventType", in: "query", required: false, - description: "Filter by event type: created, claimed, canceled, paused, resumed, start_time_updated.", + description: "Filter by event type: created, claimed, canceled, paused, resumed, start_time_updated, completed.", schema: { type: "string", - enum: ["created", "claimed", "canceled", "paused", "resumed", "start_time_updated"], + enum: ["created", "claimed", "canceled", "paused", "resumed", "start_time_updated", "completed"], }, }, { @@ -1725,6 +1725,16 @@ export const swaggerDocument = { }, }, }, + "400": { + description: "Validation error — invalid query parameters.", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Error", + }, + }, + }, + }, }, }, }, diff --git a/backend/src/validation/schemas.ts b/backend/src/validation/schemas.ts index 1b292eb..455aeba 100644 --- a/backend/src/validation/schemas.ts +++ b/backend/src/validation/schemas.ts @@ -112,7 +112,7 @@ export const updateStreamStartAtSchema = z.object({ } }); -const VALID_EVENT_TYPES = ["created", "claimed", "canceled", "start_time_updated", "paused", "resumed"] as const; +const VALID_EVENT_TYPES = ["created", "claimed", "canceled", "start_time_updated", "paused", "resumed", "completed"] as const; export const webhookRegistrationSchema = z.object({ url: z @@ -169,7 +169,11 @@ export const listEventsQuerySchema = z.object({ .min(1, "pageSize must be greater than or equal to 1") .max(100, "pageSize must be less than or equal to 100") .optional(), - streamId: z.string().trim().optional(), + streamId: z + .string() + .trim() + .min(1, "streamId must not be empty if provided") + .optional(), since: z .coerce.number() .int("since must be an integer") From 42a78a1bd4d89531aa8350e9ca2236f8f9425cd1 Mon Sep 17 00:00:00 2001 From: Shadow-MMN Date: Sun, 28 Jun 2026 18:12:02 +0100 Subject: [PATCH 3/4] fix: await async updateStreamStartAt calls in tests --- .../streamStore.updateStartAt.test.ts | 100 ++++++++---------- 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/backend/src/services/streamStore.updateStartAt.test.ts b/backend/src/services/streamStore.updateStartAt.test.ts index df2249e..84c6fba 100644 --- a/backend/src/services/streamStore.updateStartAt.test.ts +++ b/backend/src/services/streamStore.updateStartAt.test.ts @@ -7,6 +7,7 @@ const mockState = vi.hoisted(() => ({ })); const dbMocks = vi.hoisted(() => ({ + syncFtsIndex: vi.fn(), getDb: vi.fn(() => ({ transaction: vi.fn((callback: () => void) => callback), prepare: vi.fn(() => ({ @@ -53,6 +54,19 @@ const eventHistoryMocks = vi.hoisted(() => ({ }), })); +const cacheMocks = vi.hoisted(() => ({ + initCache: vi.fn(), + getCache: vi.fn(() => ({ + get: vi.fn(), + set: vi.fn(), + del: vi.fn(), + clear: vi.fn(), + isConnected: vi.fn(() => true), + })), + shutdownCache: vi.fn(), +})); + +vi.mock("./cache", () => cacheMocks); vi.mock("./db", () => dbMocks); vi.mock("./eventHistory", () => eventHistoryMocks); @@ -79,7 +93,7 @@ describe("updateStreamStartAt", () => { }); describe("Successful updates", () => { - it("should update start time of a scheduled stream and persist changes", () => { + it("should update start time of a scheduled stream and persist changes", async () => { const streamId = "1"; const oldStartAt = mockNow + 3600; // 1 hour from now const newStartAt = mockNow + 7200; // 2 hours from now @@ -97,7 +111,7 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, scheduledStream); - const result = updateStreamStartAt(streamId, newStartAt); + const result = await updateStreamStartAt(streamId, newStartAt); // Verify the stream's startAt was updated expect(result.startAt).toBe(newStartAt); @@ -118,7 +132,7 @@ describe("updateStreamStartAt", () => { ); }); - it("should record event with correct metadata containing old and new start times", () => { + it("should record event with correct metadata containing old and new start times", async () => { const streamId = "2"; const oldStartAt = mockNow + 1800; const newStartAt = mockNow + 5400; @@ -136,7 +150,7 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, scheduledStream); - updateStreamStartAt(streamId, newStartAt); + await updateStreamStartAt(streamId, newStartAt); // Verify event metadata contains both old and new start times const recordedEvent = mockState.events.find(e => e.eventType === "start_time_updated"); @@ -151,23 +165,16 @@ describe("updateStreamStartAt", () => { }); describe("Error cases", () => { - it("should throw 404 error when stream does not exist", () => { + it("should throw 404 error when stream does not exist", async () => { const nonExistentStreamId = "999"; const newStartAt = mockNow + 3600; - expect(() => { - updateStreamStartAt(nonExistentStreamId, newStartAt); - }).toThrow("Stream not found."); - - // Verify the error has correct status code - try { - updateStreamStartAt(nonExistentStreamId, newStartAt); - } catch (error: any) { - expect(error.statusCode).toBe(404); - } + await expect( + updateStreamStartAt(nonExistentStreamId, newStartAt) + ).rejects.toMatchObject({ message: "Stream not found.", statusCode: 404 }); }); - it("should throw 400 error when attempting to update start time of an active stream", () => { + it("should throw 400 error when attempting to update start time of an active stream", async () => { const streamId = "3"; const activeStream = { id: streamId, @@ -182,19 +189,12 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, activeStream); - expect(() => { - updateStreamStartAt(streamId, mockNow + 3600); - }).toThrow("Can only update start time for scheduled streams."); - - // Verify the error has correct status code - try { - updateStreamStartAt(streamId, mockNow + 3600); - } catch (error: any) { - expect(error.statusCode).toBe(400); - } + await expect( + updateStreamStartAt(streamId, mockNow + 3600) + ).rejects.toMatchObject({ message: "Can only update start time for scheduled streams.", statusCode: 400 }); }); - it("should throw 400 error when attempting to update start time of a completed stream", () => { + it("should throw 400 error when attempting to update start time of a completed stream", async () => { const streamId = "4"; const completedStream = { id: streamId, @@ -210,18 +210,12 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, completedStream); - expect(() => { - updateStreamStartAt(streamId, mockNow + 3600); - }).toThrow("Can only update start time for scheduled streams."); - - try { - updateStreamStartAt(streamId, mockNow + 3600); - } catch (error: any) { - expect(error.statusCode).toBe(400); - } + await expect( + updateStreamStartAt(streamId, mockNow + 3600) + ).rejects.toMatchObject({ message: "Can only update start time for scheduled streams.", statusCode: 400 }); }); - it("should throw 400 error when attempting to update start time of a canceled stream", () => { + it("should throw 400 error when attempting to update start time of a canceled stream", async () => { const streamId = "5"; const canceledStream = { id: streamId, @@ -237,20 +231,14 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, canceledStream); - expect(() => { - updateStreamStartAt(streamId, mockNow + 3600); - }).toThrow("Can only update start time for scheduled streams."); - - try { - updateStreamStartAt(streamId, mockNow + 3600); - } catch (error: any) { - expect(error.statusCode).toBe(400); - } + await expect( + updateStreamStartAt(streamId, mockNow + 3600) + ).rejects.toMatchObject({ message: "Can only update start time for scheduled streams.", statusCode: 400 }); }); }); describe("Event history verification", () => { - it("should make start_time_updated event queryable via getStreamHistory", () => { + it("should make start_time_updated event queryable via getStreamHistory", async () => { const streamId = "6"; const oldStartAt = mockNow + 2700; const newStartAt = mockNow + 5400; @@ -269,7 +257,7 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, scheduledStream); // Perform the update - updateStreamStartAt(streamId, newStartAt); + await updateStreamStartAt(streamId, newStartAt); // Query the event history const history = getStreamHistory(streamId); @@ -286,7 +274,7 @@ describe("updateStreamStartAt", () => { }); }); - it("should not record event when update fails due to validation error", () => { + it("should not record event when update fails due to validation error", async () => { const streamId = "7"; const activeStream = { id: streamId, @@ -302,9 +290,9 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, activeStream); // Attempt to update (should fail) - expect(() => { - updateStreamStartAt(streamId, mockNow + 3600); - }).toThrow(); + await expect( + updateStreamStartAt(streamId, mockNow + 3600) + ).rejects.toThrow(); // Verify no event was recorded expect(eventHistoryMocks.recordEventWithDb).not.toHaveBeenCalled(); @@ -313,7 +301,7 @@ describe("updateStreamStartAt", () => { }); describe("Edge cases", () => { - it("should handle updating start time to the same value", () => { + it("should handle updating start time to the same value", async () => { const streamId = "8"; const startAt = mockNow + 3600; @@ -330,7 +318,7 @@ describe("updateStreamStartAt", () => { mockState.streams.set(streamId, scheduledStream); - const result = updateStreamStartAt(streamId, startAt); + const result = await updateStreamStartAt(streamId, startAt); // Should still work and record event expect(result.startAt).toBe(startAt); @@ -345,7 +333,7 @@ describe("updateStreamStartAt", () => { ); }); - it("should handle updating start time to a past timestamp for scheduled stream", () => { + it("should handle updating start time to a past timestamp for scheduled stream", async () => { const streamId = "9"; const oldStartAt = mockNow + 3600; const newStartAt = mockNow - 1800; // Past time @@ -365,7 +353,7 @@ describe("updateStreamStartAt", () => { // This should work - the function doesn't validate against past times // (that validation might be at the API layer or business logic layer) - const result = updateStreamStartAt(streamId, newStartAt); + const result = await updateStreamStartAt(streamId, newStartAt); expect(result.startAt).toBe(newStartAt); expect(eventHistoryMocks.recordEventWithDb).toHaveBeenCalled(); From 984f7e25872270c596cae35a9395cee94bba039a Mon Sep 17 00:00:00 2001 From: Shadow-MMN Date: Sun, 28 Jun 2026 18:22:50 +0100 Subject: [PATCH 4/4] fix: resolve pre-existing test failures across multiple test files - streamStore.updateStartAt.test.ts: await async updateStreamStartAt calls - streamStore.progress.test.ts: cap elapsedSeconds at duration, handle zero duration - eventHistory.test.ts: pass asc order explicitly to getStreamHistory calls - streamStore.test.ts, streamStore.reconcile.test.ts: mock logger module - cors.test.ts: remove duplicate app.use(cors()) causing origin override - requestLogger.test.ts: mock setHeader in first test case - Add syncFtsIndex to dbMocks in 3 test files --- backend/src/index.ts | 1 - backend/src/middleware/requestLogger.test.ts | 2 ++ backend/src/services/eventHistory.test.ts | 10 +++++----- .../src/services/streamStore.reconcile.test.ts | 15 +++++++++++++-- backend/src/services/streamStore.test.ts | 18 +++++++++++++++--- backend/src/services/streamStore.ts | 7 ++++--- 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 61eca60..71d7e52 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -283,7 +283,6 @@ app.use(helmet({ preload: true, }, })); -app.use(cors()); const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS; if (ALLOWED_ORIGINS) { diff --git a/backend/src/middleware/requestLogger.test.ts b/backend/src/middleware/requestLogger.test.ts index 4c507e3..d368fbc 100644 --- a/backend/src/middleware/requestLogger.test.ts +++ b/backend/src/middleware/requestLogger.test.ts @@ -7,6 +7,7 @@ import type { Request, Response } from "express"; describe("requestLogger", () => { const originalNodeEnv = process.env.NODE_ENV; const loggerInfoSpy = vi.spyOn(logger, "info").mockImplementation(() => logger); + vi.spyOn(logger, "child").mockImplementation(() => logger); beforeEach(() => { loggerInfoSpy.mockClear(); @@ -29,6 +30,7 @@ describe("requestLogger", () => { const res = new EventEmitter() as Response; (res as any).statusCode = 201; + (res as any).setHeader = vi.fn(); const next = vi.fn(); diff --git a/backend/src/services/eventHistory.test.ts b/backend/src/services/eventHistory.test.ts index 1190cbb..81358ea 100644 --- a/backend/src/services/eventHistory.test.ts +++ b/backend/src/services/eventHistory.test.ts @@ -102,7 +102,7 @@ describe("eventHistory", () => { expect(countStreamEvents("stream-1")).toBe(2); - const history = getStreamHistory("stream-1"); + const history = getStreamHistory("stream-1", 20, 0, "asc"); expect(history).toHaveLength(2); expect(history.map((e) => e.eventType)).toEqual(["created", "claimed"]); }); @@ -149,7 +149,7 @@ describe("eventHistory", () => { recordEvent("stream-4", "start_time_updated", 2000); recordEvent("stream-4", "canceled", 4000); - const history = getStreamHistory("stream-4"); + const history = getStreamHistory("stream-4", 20, 0, "asc"); expect(history.map((e) => e.timestamp)).toEqual([1000, 2000, 3000, 4000]); expect(history.map((e) => e.eventType)).toEqual([ @@ -167,7 +167,7 @@ describe("eventHistory", () => { recordEvent("stream-5", "claimed", 1000, "second"); recordEvent("stream-5", "canceled", 1000, "third"); - const history = getStreamHistory("stream-5"); + const history = getStreamHistory("stream-5", 20, 0, "asc"); expect(history.map((e) => e.actor)).toEqual(["first", "second", "third"]); }); @@ -179,8 +179,8 @@ describe("eventHistory", () => { recordEvent("stream-B", "created", 500); recordEvent("stream-A", "claimed", 2000); - const historyA = getStreamHistory("stream-A"); - const historyB = getStreamHistory("stream-B"); + const historyA = getStreamHistory("stream-A", 20, 0, "asc"); + const historyB = getStreamHistory("stream-B", 20, 0, "asc"); expect(historyA).toHaveLength(2); expect(historyA.map((e) => e.timestamp)).toEqual([1000, 2000]); diff --git a/backend/src/services/streamStore.reconcile.test.ts b/backend/src/services/streamStore.reconcile.test.ts index b0f57e0..42311ca 100644 --- a/backend/src/services/streamStore.reconcile.test.ts +++ b/backend/src/services/streamStore.reconcile.test.ts @@ -29,6 +29,7 @@ const mockState = vi.hoisted(() => ({ const dbMocks = vi.hoisted(() => ({ initDb: vi.fn(), getDb: vi.fn(), + syncFtsIndex: vi.fn(), })); const eventHistoryMocks = vi.hoisted(() => ({ @@ -38,9 +39,19 @@ const eventHistoryMocks = vi.hoisted(() => ({ }), })); +const loggerMocks = vi.hoisted(() => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, +})); + vi.mock("./db", () => dbMocks); vi.mock("./eventHistory", () => eventHistoryMocks); vi.mock("./webhook", () => ({ triggerWebhook: vi.fn() })); +vi.mock("../logger", () => loggerMocks); vi.mock("@stellar/stellar-sdk", () => { class MockContract { @@ -316,7 +327,7 @@ describe("reconcileMissingStreams – sync correctness", () => { // Trigger timeout on all RPC calls mockState.simulateTimeout = true; - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + const errorSpy = loggerMocks.logger.error; const { initSoroban, reconcileMissingStreams } = await import("./streamStore"); await initSoroban(); @@ -327,7 +338,7 @@ describe("reconcileMissingStreams – sync correctness", () => { expect(errorSpy).toHaveBeenCalled(); expect(mockState.upsertedStreams).toHaveLength(0); - errorSpy.mockRestore(); + errorSpy.mockClear(); }); it("is idempotent — running twice does not duplicate rows or events", async () => { diff --git a/backend/src/services/streamStore.test.ts b/backend/src/services/streamStore.test.ts index 16e492c..29fba0a 100644 --- a/backend/src/services/streamStore.test.ts +++ b/backend/src/services/streamStore.test.ts @@ -24,6 +24,7 @@ const mockState = vi.hoisted(() => ({ const dbMocks = vi.hoisted(() => ({ initDb: vi.fn(), getDb: vi.fn(), + syncFtsIndex: vi.fn(), })); const eventHistoryMocks = vi.hoisted(() => ({ @@ -33,11 +34,21 @@ const eventHistoryMocks = vi.hoisted(() => ({ }), })); +const loggerMocks = vi.hoisted(() => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, +})); + vi.mock("./db", () => dbMocks); vi.mock("./eventHistory", () => eventHistoryMocks); vi.mock("./webhook", () => ({ triggerWebhook: vi.fn(), })); +vi.mock("../logger", () => loggerMocks); vi.mock("@stellar/stellar-sdk", () => { class MockContract { @@ -272,7 +283,7 @@ describe("reconcileMissingStreams", () => { mockState.nextId = 3; mockState.existingStreamIds = new Set(["1"]); - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + const errorSpy = loggerMocks.logger.error; const { initSoroban, reconcileMissingStreams } = await import("./streamStore"); @@ -281,11 +292,12 @@ describe("reconcileMissingStreams", () => { expect(repaired).toBe(0); expect(errorSpy).toHaveBeenCalledWith( - "[reconciliation] missing stream 2 could not be fetched from chain", + { streamId: 2 }, + "missing stream could not be fetched from chain", ); expect(eventHistoryMocks.recordEventWithDb).not.toHaveBeenCalled(); - errorSpy.mockRestore(); + errorSpy.mockClear(); }); }); diff --git a/backend/src/services/streamStore.ts b/backend/src/services/streamStore.ts index 4ca82e3..a92b48f 100644 --- a/backend/src/services/streamStore.ts +++ b/backend/src/services/streamStore.ts @@ -446,13 +446,14 @@ export function calculateProgress( : at; const elapsed = Math.max(0, Math.max(0, effectiveAt - stream.startAt) - stream.pausedDuration); - const ratio = Math.min(1, elapsed / stream.durationSeconds); + const ratio = stream.durationSeconds <= 0 ? 1 : Math.min(1, elapsed / stream.durationSeconds); + const elapsedSeconds = stream.durationSeconds <= 0 ? 0 : Math.min(elapsed, stream.durationSeconds); const vestedAmount = stream.totalAmount * ratio; return { status: computeStatus(stream, at), - ratePerSecond: round(stream.totalAmount / stream.durationSeconds), - elapsedSeconds: elapsed, + ratePerSecond: stream.durationSeconds <= 0 ? Infinity : round(stream.totalAmount / stream.durationSeconds), + elapsedSeconds, vestedAmount: round(vestedAmount), remainingAmount: round(Math.max(0, stream.totalAmount - vestedAmount)), percentComplete: round(ratio * 100),