diff --git a/.gitignore b/.gitignore index cb5c3c9..2510ff2 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ Notes.md .env.* # development and testing files +.notes aws.temp bulk/converted*.jsonl cves/deltaLog.json diff --git a/ChangeLog.md b/ChangeLog.md index 822480a..f910bf0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,7 +1,12 @@ # Change Log -### 2.2.0-rc1 (Sprint 1) +### 2.2.0-rc3 (Sprint 4) - exact phrase search using double quotes + - date and date range search on date fields, using the following formats (date ranges are inclusive): + - YYYY-MM-DD + - YYYY-MM-DDTHH:MM:SS(.mmm)(Z) (where the .mmm and Z are optional, defaults to .000Z if missing) + - YYYY-MM-DD..YYYY-MM-DD + - YYYY-MM-DDTHH:MM:SS(.mmm)(Z)..YYYY-MM-DDTHH:MM:SS.(mmm)(Z) ### 2.1.0 - wildcard search using "*" and "?" diff --git a/index.ts b/index.ts index 7965f2d..20bbbf3 100644 --- a/index.ts +++ b/index.ts @@ -26,6 +26,9 @@ export * from "./src/commands/GenericCommand.js"; export * from "./src/commands/MainCommands.js"; // common +export * from "./src/common/IsoDate/IsoDate.js"; +export * from "./src/common/IsoDate/IsoDatetime.js"; +export * from "./src/common/IsoDate/IsoDatetimeRange.js"; export * from "./src/common/IsoDate/IsoDateString.js"; export * from "./src/common/Json/Json.js"; export * from "./src/common/comparer/ObjectComparer.js"; diff --git a/package-lock.json b/package-lock.json index a0ba7ef..c94e1c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-core", - "version": "2.2.0-rc1", + "version": "2.2.0-rc3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-core", - "version": "2.2.0-rc1", + "version": "2.2.0-rc3", "license": "(CC0)", "dependencies": { "@commander-js/extra-typings": "^10.0.2", @@ -587,9 +587,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -604,9 +604,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -621,9 +621,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -638,9 +638,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -655,9 +655,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -672,9 +672,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -689,9 +689,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -706,9 +706,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -723,9 +723,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -740,9 +740,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -757,9 +757,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -774,9 +774,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -791,9 +791,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -808,9 +808,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -825,9 +825,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -842,9 +842,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -859,9 +859,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -876,9 +876,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -893,9 +893,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -910,9 +910,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -927,9 +927,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -944,9 +944,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], @@ -961,9 +961,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -978,9 +978,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -995,9 +995,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -1012,9 +1012,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -1686,9 +1686,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", - "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", "cpu": [ "arm" ], @@ -1700,9 +1700,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", - "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", "cpu": [ "arm64" ], @@ -1714,9 +1714,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", - "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", "cpu": [ "arm64" ], @@ -1728,9 +1728,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", - "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", "cpu": [ "x64" ], @@ -1742,9 +1742,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", - "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", "cpu": [ "arm64" ], @@ -1756,9 +1756,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", - "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", "cpu": [ "x64" ], @@ -1770,9 +1770,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", - "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", "cpu": [ "arm" ], @@ -1784,9 +1784,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", - "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", "cpu": [ "arm" ], @@ -1798,9 +1798,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", - "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", "cpu": [ "arm64" ], @@ -1812,9 +1812,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", - "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", "cpu": [ "arm64" ], @@ -1825,10 +1825,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", - "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", "cpu": [ "loong64" ], @@ -1840,9 +1840,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", - "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", "cpu": [ "ppc64" ], @@ -1854,9 +1854,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", - "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", "cpu": [ "riscv64" ], @@ -1868,9 +1868,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", - "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", "cpu": [ "riscv64" ], @@ -1882,9 +1882,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", - "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", "cpu": [ "s390x" ], @@ -1896,9 +1896,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", - "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", "cpu": [ "x64" ], @@ -1910,9 +1910,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", - "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", "cpu": [ "x64" ], @@ -1924,9 +1924,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", - "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", "cpu": [ "arm64" ], @@ -1938,9 +1938,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", - "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", "cpu": [ "arm64" ], @@ -1952,9 +1952,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", - "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", "cpu": [ "ia32" ], @@ -1965,10 +1965,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", - "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", "cpu": [ "x64" ], @@ -2114,9 +2128,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.124", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.124.tgz", - "integrity": "sha512-hY4YWZFLs3ku6D2Gqo3RchTd9VRCcrjqp/I0mmohYeUVA5Y8eCXKJEasHxLAJVZRJuQogfd1GiJ9lgogBgKeuQ==", + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "dev": true, "license": "MIT", "dependencies": { @@ -2516,9 +2530,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz", - "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2645,9 +2659,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", - "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", + "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2679,9 +2693,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -2699,9 +2713,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.2", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -2802,9 +2816,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", "dev": true, "funding": [ { @@ -2955,9 +2969,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, "license": "MIT" }, @@ -3083,9 +3097,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3211,9 +3225,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "dev": true, "license": "ISC" }, @@ -3237,9 +3251,9 @@ "license": "MIT" }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3292,9 +3306,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3305,32 +3319,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/escalade": { @@ -5979,9 +5993,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", "dev": true, "license": "MIT" }, @@ -6208,9 +6222,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", - "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "license": "ISC", "engines": { "node": "20 || >=22" @@ -6595,9 +6609,9 @@ } }, "node_modules/rollup": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", - "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", "dependencies": { @@ -6611,27 +6625,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.1", - "@rollup/rollup-android-arm64": "4.50.1", - "@rollup/rollup-darwin-arm64": "4.50.1", - "@rollup/rollup-darwin-x64": "4.50.1", - "@rollup/rollup-freebsd-arm64": "4.50.1", - "@rollup/rollup-freebsd-x64": "4.50.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", - "@rollup/rollup-linux-arm-musleabihf": "4.50.1", - "@rollup/rollup-linux-arm64-gnu": "4.50.1", - "@rollup/rollup-linux-arm64-musl": "4.50.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", - "@rollup/rollup-linux-ppc64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-musl": "4.50.1", - "@rollup/rollup-linux-s390x-gnu": "4.50.1", - "@rollup/rollup-linux-x64-gnu": "4.50.1", - "@rollup/rollup-linux-x64-musl": "4.50.1", - "@rollup/rollup-openharmony-arm64": "4.50.1", - "@rollup/rollup-win32-arm64-msvc": "4.50.1", - "@rollup/rollup-win32-ia32-msvc": "4.50.1", - "@rollup/rollup-win32-x64-msvc": "4.50.1", + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" } }, @@ -6666,9 +6681,9 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7833,4 +7848,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 010b0c8..dacde47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cve-core", - "version": "2.2.0-rc1", + "version": "2.2.0-rc3", "description": "CVE npm package for working with CVEs", "type": "module", "engines": { diff --git a/src/adapters/search/OpensearchDatetimeUtils.test.unit.ts b/src/adapters/search/OpensearchDatetimeUtils.test.unit.ts index f6a6b01..8d39f0c 100644 --- a/src/adapters/search/OpensearchDatetimeUtils.test.unit.ts +++ b/src/adapters/search/OpensearchDatetimeUtils.test.unit.ts @@ -5,7 +5,10 @@ import { IsoDate, IsoDatetime } from '../../common/IsoDate/IsoDatetime'; describe('OpensearchDatetimeUtils.toSearchDateDslString()', () => { const testcases = [ + { input: '2025-03-01T12:34:56.001Z', expected: '2025-03-01T12:34:56.001Z' }, + { input: '2025-03-01T12:34:56.001', expected: '2025-03-01T12:34:56.001Z' }, { input: '2025-03-01T12:34:56Z', expected: '2025-03-01T12:34:56Z' }, + { input: '2025-03-01T12:34:56', expected: '2025-03-01T12:34:56Z' }, { input: '2025-03-01', expected: '2025-03-01||/d' }, { input: '2025-03', expected: '2025-03||/M' }, { input: '2025', expected: '2025||/y' }, diff --git a/src/common/IsoDate/IsoDate.test.unit.ts b/src/common/IsoDate/IsoDate.test.unit.ts index dce7657..83e1f58 100644 --- a/src/common/IsoDate/IsoDate.test.unit.ts +++ b/src/common/IsoDate/IsoDate.test.unit.ts @@ -75,47 +75,87 @@ describe('IsoDate.parse – valid inputs', () => { }); }); -describe('IsoDate.parse – invalid inputs', () => { - const invalid = [ - '202501', // year+month without hyphen - '20250101', // compact full date (properly rejected in this class) - '250101', // two‑digit year - '2025-13-01', // invalid month - '2025-02-30', // invalid day (Feb 30) - '2025-04-31', // invalid day (April 31) - '-2025-04-31', // invalid year - '--01-01', // leading hyphens - '-2025-01', // leading hyphen before year - '2025--01', // double hyphen between year and month - '2025-01--01', // double hyphen before day - '2025-02-29', // illegal leap year - '2025-01-01T014:00:00:00Z', // datetime does not match in this class - ]; - invalid.forEach((value) => { - test(`throws for "${value}"`, () => { - expect(() => IsoDate.parse(value)).toThrow(Error); - }); - }); +describe('IsoDate.parse/toString/isMonth/isDate/isYear', () => { + const tests: Array<{ value: string; isYear?: boolean; isMonth?: boolean; isDate?: boolean; isDatetime?: boolean; threw?: RegExp; }> = [ + // valid ISO Date specs + { value: `2025`, isYear: true }, + { value: ` 2025 `, isYear: true }, + { value: `2025-10`, isMonth: true }, + { value: `2024-02-29`, isDate: true }, + // invalid ISO Date specs + { value: `25`, threw: /Invalid calendar date format/ }, // year must be after 1000 and before 2500 + { value: `1899`, threw: /Year out of range/ }, // year must be after 1900 and before 2100 + { value: `2500`, threw: /Year out of range/ }, // year must be after 1900 and before 2100 + { value: `1/1/25`, threw: /Invalid calendar date format/ }, // bad format + { value: `abc`, threw: /Invalid calendar date format/ }, // bad format + { value: `202501`, threw: /Invalid calendar date format/ }, // year+month without hyphen + { value: `20250101`, threw: /Invalid calendar date format/ }, // compact full date (properly rejected in this class) + { value: `250101`, threw: /Invalid calendar date format/ }, // two‑digit year + { value: `-2025-04-31`, threw: /Invalid calendar date format/ }, // leading hyphen + { value: `-2025-04`, threw: /Invalid calendar date format/ }, // leading hyphen + { value: `01-01`, threw: /Invalid calendar date format/ }, + { value: `2025--01`, threw: /Invalid calendar date format/ }, + { value: `2025-01-01T014:00:00:00Z`, threw: /Invalid calendar date format/ }, + { value: `2025-13-01`, threw: /Month out of range/ }, + { value: `2025-02-29`, threw: /Day out of range/ }, // not a leap year + { value: `2025-02-30`, threw: /Day out of range/ }, + { value: `2025-04-31`, threw: /Day out of range/ }, + ]; - invalid.forEach((value) => { - test(`"${value}" is not an IsoDate`, () => { - expect(isValidIsoDate(value)).toBeFalsy(); + tests.forEach(({ value, isYear = false, isMonth = false, isDate = false, threw = '' }) => { + test(`properly determines if ${value} is a ${isYear ? 'year' : ''}${isMonth ? 'month' : ''}${isDate ? 'date' : ''}${threw ? 'error' : ''}`, () => { + if (threw) { + expect(() => IsoDate.parse(value)).toThrow(threw); + } + else { + const isoDate = IsoDate.parse(value); + if (isYear) { + expect(isoDate.isYear()).toBeTruthy(); + expect(isoDate.isMonth()).toBeFalsy(); + expect(isoDate.isDate()).toBeFalsy(); + expect(isoDate.isDatetime()).toBeFalsy(); + } + else if (isMonth) { + expect(isoDate.isYear()).toBeFalsy(); + expect(isoDate.isMonth()).toBeTruthy(); + expect(isoDate.isDate()).toBeFalsy(); + expect(isoDate.isDatetime()).toBeFalsy(); + } + else if (isDate) { + expect(isoDate.isYear()).toBeFalsy(); + expect(isoDate.isMonth()).toBeFalsy(); + expect(isoDate.isDate()).toBeTruthy(); + expect(isoDate.isDatetime()).toBeFalsy(); + } + expect(isoDate.toString()).toBe(value.trim()); + } }); }); }); -describe('IsoDate.toString', () => { - const tests: Array<{ input: string; expected: string; }> = [ - { input: '2025-01-01', expected: '2025-01-01' }, - { input: '2025-01', expected: '2025-01' }, - { input: '2025', expected: '2025' } + +describe('IsoDate.daysInMonth()', () => { + const tests: Array<{ year: number; month: number, expected: number; }> = [ + { year: 2025, month: 1, expected: 31 }, + { year: 2025, month: 2, expected: 28 }, + { year: 2025, month: 3, expected: 31 }, + { year: 2025, month: 4, expected: 30 }, + { year: 2025, month: 5, expected: 31 }, + { year: 2025, month: 6, expected: 30 }, + { year: 2025, month: 7, expected: 31 }, + { year: 2025, month: 8, expected: 31 }, + { year: 2025, month: 9, expected: 30 }, + { year: 2025, month: 10, expected: 31 }, + { year: 2025, month: 11, expected: 30 }, + { year: 2025, month: 12, expected: 31 }, + { year: 2024, month: 2, expected: 29 }, // leap year ]; - tests.forEach(({input, expected}) => { - test(`properly prints out '${input}' as '${expected}'`, () => { - const isoDate = IsoDate.parse(input) - expect(isoDate.toString()).toBe(expected) + tests.forEach(({ year, month, expected }) => { + test(`properly calculates ${year}-${month} to have ${expected} days`, () => { + const days = IsoDate.daysInMonth(year, month); + expect(days).toBe(expected); }); }); -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/src/common/IsoDate/IsoDate.ts b/src/common/IsoDate/IsoDate.ts index 496d093..e7d3efe 100644 --- a/src/common/IsoDate/IsoDate.ts +++ b/src/common/IsoDate/IsoDate.ts @@ -34,7 +34,20 @@ */ export class IsoDate { - + + /** the string specified by user (may be original or may be modified (e.g., trimmed)) + * this is necessary in order for other functions to determine user's intention + * (e.g., when search date ranges, a 2025-02 value in opensearch needs to be specified as + * 2025-02||/M see adapters/search/OpensearchDatetimeUtils) + */ + private _parsedValue: string; + public get parsedValue(): string { + return this._parsedValue; + } + protected set parsedValue(value: string) { + this._parsedValue = value; + } + /** Full year (e.g., 2025) */ public readonly year: number; /** Month number 1‑12 (optional) */ @@ -42,10 +55,11 @@ export class IsoDate { /** Day number 1‑31 (optional, requires month) */ public readonly day?: number; - protected constructor(year: number, month?: number, day?: number) { + protected constructor(parsedValue: string, year: number, month?: number, day?: number) { this.year = year; if (month !== undefined) this.month = month; if (day !== undefined) this.day = day; + this.parsedValue = (parsedValue) ?? this.toString(); } /** @@ -64,6 +78,7 @@ export class IsoDate { // /^(?\d{4})(?:[-]?(?\d{2})(?:[-]?(?\d{2})?)?)?$/; /^(?\d{4})(?:[-](?\d{2})(?:[-](?\d{2})?)?)?$/; + value = value.trim(); const match = regex.exec(value); if (!match || !match.groups) { throw new Error(`Invalid calendar date format: "${value}": must be one of YYYY-MM-DD, YYYY-MM, or YYYY`); @@ -74,7 +89,7 @@ export class IsoDate { const dayStr = match.groups.day; // Validate year range (reasonable limits) - if (year < 1 || year > 2500) { + if (year < 1900 || year > 2100) { throw new Error(`Year out of range: ${year}`); } @@ -97,15 +112,15 @@ export class IsoDate { )}: ${dayStr}` ); } - return new IsoDate(year, month, day); + return new IsoDate(value, year, month, day); } // Month only (no day) - return new IsoDate(year, month); + return new IsoDate(value, year, month); } // Year only - return new IsoDate(year); + return new IsoDate(value, year); } /** Return true if the stored year is a leap year. */ @@ -113,19 +128,24 @@ export class IsoDate { return IsoDate.isLeapYear(this.year); } - /** Return true if the stored year is a leap year. */ + /** Return true if the stored date is a year. */ public isYear(): boolean { - return this.toString().length === 4; + return this.parsedValue.length === 4; } - /** Return true if the stored year is a leap year. */ + /** Return true if the stored date is a month. */ public isMonth(): boolean { - return this.toString().length === 7; + return this.parsedValue.length === 7; } - /** Return true if the stored year is a leap year. */ + /** Return true if the stored date is a date. */ public isDate(): boolean { - return this.toString().length === 10; + return this.parsedValue.length === 10; + } + + /** Return true if the stored date is IsoDatetime. */ + public isDatetime(): boolean { + return this.parsedValue.length > 10; } diff --git a/src/common/IsoDate/IsoDatetime.test.unit.ts b/src/common/IsoDate/IsoDatetime.test.unit.ts index fedf9ad..b96145e 100644 --- a/src/common/IsoDate/IsoDatetime.test.unit.ts +++ b/src/common/IsoDate/IsoDatetime.test.unit.ts @@ -65,6 +65,7 @@ describe('IsoDatetime.parse – valid inputs', () => { const valid = [ { input: '2024-02-29T23:00:00+01:00', expected: '2024-02-29T22:00:00Z' }, // leap year date with offset { input: '2025-03-01', expected: '2025-03-01T00:00:00Z' }, // date only + { input: '2025-03-01T12:34:56Z', expected: '2025-03-01T12:34:56Z' }, // missing milliseconds { input: '2025-03-01T12:34:56.789', expected: '2025-03-01T12:34:56.789Z' }, // missing Z // currently does not allow the following even though it is valid in ISO 8601 // { input: '2025-03', expected: '2024-03' }, // month only @@ -156,6 +157,54 @@ describe('IsoDatetime.toIsoDate', () => { }); }); + +describe('IsoDatetime.isMonth/isDate/isYear', () => { + const tests: Array<{ value: string; isYear?: boolean; isMonth?: boolean; isDate?: boolean; isDatetime?: boolean; threw?: string; }> = [ + { value: `2025`, isYear: true }, + { value: `2025-10`, isMonth: true }, + { value: `2025-10-02`, isDate: true }, + { value: `2025-10-02T10:11:12Z`, isDatetime: true }, + { value: `2025-02-29`, threw: "[Error: Day out of range" }, // not a leap year + ]; + + tests.forEach(({ value, isYear = false, isMonth = false, isDate = false, isDatetime = false, threw = '' }) => { + test(`properly determines if ${value} is a ${isYear ? 'year' : ''}${isMonth ? 'month' : ''}${isDate ? 'date' : ''}`, () => { + try { + const isoDatetime = IsoDatetime.parse(value); + if (isYear) { + expect(isoDatetime.isYear()).toBeTruthy(); + expect(isoDatetime.isMonth()).toBeFalsy(); + expect(isoDatetime.isDate()).toBeFalsy(); + expect(isoDatetime.isDatetime()).toBeFalsy(); + } + else if (isMonth) { + expect(isoDatetime.isYear()).toBeFalsy(); + expect(isoDatetime.isMonth()).toBeTruthy(); + expect(isoDatetime.isDate()).toBeFalsy(); + expect(isoDatetime.isDatetime()).toBeFalsy(); + } + else if (isDate) { + expect(isoDatetime.isYear()).toBeFalsy(); + expect(isoDatetime.isMonth()).toBeFalsy(); + expect(isoDatetime.isDate()).toBeTruthy(); + expect(isoDatetime.isDatetime()).toBeFalsy(); + } + else if (isDatetime) { + expect(isoDatetime.isYear()).toBeFalsy(); + expect(isoDatetime.isMonth()).toBeFalsy(); + expect(isoDatetime.isDate()).toBeFalsy(); + expect(isoDatetime.isDatetime()).toBeTruthy(); + } + } + catch (e: unknown) { + if (e instanceof Error) { + expect(e.message.startsWith(threw)); + } + } + }); + }); +}); + describe('IsoDatetime.getNextDay – day increments and decrements', () => { test('regular date: March 4 +1 day => March 5', () => { const dt = IsoDatetime.parse('2024-03-04T00:00:00Z'); diff --git a/src/common/IsoDate/IsoDatetime.ts b/src/common/IsoDate/IsoDatetime.ts index 413c144..aa52446 100644 --- a/src/common/IsoDate/IsoDatetime.ts +++ b/src/common/IsoDate/IsoDatetime.ts @@ -39,6 +39,7 @@ export class IsoDatetime extends IsoDate { public readonly millisecond: number; private constructor( + parsedValue: string, year: number, month: number, day: number, @@ -47,11 +48,12 @@ export class IsoDatetime extends IsoDate { second: number, millisecond: number, ) { - super(year, month, day); + super(parsedValue, year, month, day); this.hour = hour; this.minute = minute; this.second = second; this.millisecond = millisecond; + this.parsedValue = (parsedValue) ?? this.toString(); } /** @@ -69,6 +71,8 @@ export class IsoDatetime extends IsoDate { const regex = /^(?\d{4})-(?\d{2})-(?\d{2})(?:T(?\d{2}):(?\d{2}):(?\d{2})(?:\.(?\d+))?(?Z|[+-]\d{2}:\d{2})?)?$/; + value = value.trim(); + // console.log(`isoDatetime.parse(${value})`) const match = regex.exec(value); if (!match || !match.groups) { throw new Error( @@ -87,7 +91,7 @@ export class IsoDatetime extends IsoDate { const tz = match.groups.tz ?? 'Z'; // Validate date components using IsoDate's helpers. - if (year < 1 || year > 2500) { + if (year < 1900 || year > 2100) { throw new Error(`Year out of range: ${year}`); } if (month < 1 || month > 12) { @@ -134,6 +138,7 @@ export class IsoDatetime extends IsoDate { const normMs = utcDate.getUTCMilliseconds(); return new IsoDatetime( + value, normYear, normMonth, normDay, @@ -160,7 +165,12 @@ export class IsoDatetime extends IsoDate { else if (date.isYear()) { isoString = `${date.toString()}-01-01T00:00:00.000Z`; } - return IsoDatetime.parse(isoString); + // it's important to remember that this came from an IsoDate + // (thus, resetting the parsedValue) since how it was specified + // is important for some purposes, and cannot be reliably calculated after this point + let isoDatetime = IsoDatetime.parse(isoString); + isoDatetime.parsedValue = date.parsedValue; + return isoDatetime; } /** Normalized ISO‑8601 string in UTC (always ends with ‘Z’). */ @@ -176,7 +186,7 @@ export class IsoDatetime extends IsoDate { } /** returns an IsoDate - * Note: lossy precision + * NOTE: lossy precision * Even though this reduces the precision of a Datetime, it is useful for some purposes */ public toIsoDate(): IsoDate { @@ -187,9 +197,8 @@ export class IsoDatetime extends IsoDate { return IsoDate.parse(dateString); } - /** - * Returns an ISO‑8601 string representing this datetime shifted by the given + * Returns an IsoDatetime object representing this datetime shifted by the given * number of days (positive or negative). The shift respects month lengths, * leap‑year rules, and century leap‑year exceptions. * @@ -206,10 +215,28 @@ export class IsoDatetime extends IsoDate { this.second, this.millisecond, ); - // Add the day offset (24 h = 86 400 000 ms). - const newMs = utcMs + increment * 86_400_000; + let newMs = 0; + if (this.isDate()) { + // Add the day offset (24 h = 86 400 000 ms). + newMs = utcMs + increment * 86_400_000; + } + // else if (this.isMonth()) { + // // Add the month offset (24 h = 86 400 000 ms). + // const newMs = utcMs + increment * 86_400_000 * IsoDate.daysInMonth(this.year, this.month); + // } + // else if (this.isYear()) { + // // // Add the day offset (24 h = 86 400 000 ms). + // // const newMs = utcMs + increment * 86_400_000; + // } + else { // all other examples are Datetimes, and thus has milliseconds + // Add the day offset (24 h = 86 400 000 ms). + // newMs = utcMs + 1; + newMs = utcMs + increment * 86_400_000; + } + const d = new Date(newMs); const next = new IsoDatetime( + undefined, // will be filled in by constructor using the new values d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), @@ -224,7 +251,7 @@ export class IsoDatetime extends IsoDate { } -/* Utility function that always returns a IsoDatetime version +/** Utility function that always returns a IsoDatetime version * of a IsoDate or IsoDatetime object, very useful * when the type of the IsoDate object can be either */ @@ -236,3 +263,22 @@ export function toIsoDatetime(o: IsoDate | IsoDatetime): IsoDatetime { return IsoDatetime.fromIsoDate(o); } } + +/** Returns true iff text can be converted into an IsoDatetime object + * @param text + * @returns true iff text can be converted into an IsoDatetime object +*/ +export function isIsoDatetime(text: string): boolean { + if (!text || typeof text !== 'string' || text.length <= 0) { + return false; + } + else { + try { + const date = IsoDatetime.parse(text); + return true; + } + catch (e) { + return false; + } + } +} diff --git a/src/common/IsoDate/IsoDatetimeRange.test.unit.ts b/src/common/IsoDate/IsoDatetimeRange.test.unit.ts new file mode 100644 index 0000000..90f3f40 --- /dev/null +++ b/src/common/IsoDate/IsoDatetimeRange.test.unit.ts @@ -0,0 +1,139 @@ +import { describe, it, test, expect } from '@jest/globals'; +import { IsoDate, isValidIsoDate } from './IsoDate.js'; +import { IsoDatetime, toIsoDatetime, isIsoDatetime } from './IsoDatetime.js'; +import { IsoDatetimeRange, isIsoDatetimeRange } from './IsoDatetimeRange.js'; + +describe('IsoDatetimeRange.parse (in depth)', () => { + test('parses year‑only range', () => { + const range = IsoDatetimeRange.parse('2020..2025'); + if (range) { + expect(range.getStart().toString()).toBe('2020-01-01T00:00:00Z'); + expect(range.getEnd().toString()).toBe('2025-01-01T00:00:00Z'); + } + else { + throw new Error(`IsoDatetimeRange.parse() returned an unexpected result`); + } + }); + + test('parses year‑month range', () => { + const range = IsoDatetimeRange.parse('2020-03..2020-05'); + if (range) { + expect(range.getStart().toString()).toBe('2020-03-01T00:00:00Z'); + expect(range.getEnd().toString()).toBe('2020-05-01T00:00:00Z'); + } + else { + throw new Error(`IsoDatetimeRange.parse() returned an unexpected result`); + } + }); + + test('parses full date range', () => { + const range = IsoDatetimeRange.parse('2020-03-15..2020-04-20'); + if (range) { + expect(range.getStart().toString()).toBe('2020-03-15T00:00:00Z'); + expect(range.getEnd().toString()).toBe('2020-04-20T00:00:00Z'); + } + else { + throw new Error(`IsoDatetimeRange.parse() returned an unexpected result`); + } + }); + + test('parses full datetime range with offsets', () => { + const range = IsoDatetimeRange.parse('2020-03-15T12:00:00+02:00..2020-03-15T14:30:00Z'); + // 12:00+02:00 => 10:00Z, 14:30Z stays 14:30Z + if (range) { + expect(range.getStart().toString()).toBe('2020-03-15T10:00:00Z'); + expect(range.getEnd().toString()).toBe('2020-03-15T14:30:00Z'); + } + else { + throw new Error(`IsoDatetimeRange.parse() returned an unexpected result`); + } + }); + + test('toArray returns start and end', () => { + const range = IsoDatetimeRange.parse('2021-01-01..2021-01-02'); + if (range) { + const arr = range.toArray(); + expect(arr).toHaveLength(2); + expect(arr[0]).toBeInstanceOf(IsoDatetime); + expect(arr[1]).toBeInstanceOf(IsoDatetime); + expect(arr[0].toString()).toBe('2021-01-01T00:00:00Z'); + expect(arr[1].toString()).toBe('2021-01-02T00:00:00Z'); + } + else { + throw new Error(`IsoDatetimeRange.parse() returned an unexpected result`); + } + }); + + test('throws when start is after end', () => { + expect(() => IsoDatetimeRange.parse('2025-01-01..2024-12-31')).toThrow( + /Invalid range/, + ); + }); + +}); + + + +describe('IsoDatetimeRange.parse(), toString(), isIsoDatetimeRange()', () => { + const testCases: Array<{ input?: string, expected?: string, exception?: RegExp; isDate?: boolean; isDateRange?: boolean; }> = [ + // valid dates range specifications (exception assumed false unless specified) + { input: `2025`, expected: `2025-01-01T00:00:00Z..2025-01-01T00:00:00Z` }, + { input: `2025-05`, expected: `2025-05-01T00:00:00Z..2025-05-01T00:00:00Z` }, + { input: `2025-05-14`, expected: `2025-05-14T00:00:00Z..2025-05-14T00:00:00Z`, isDate: true }, + { input: `2025..2025`, expected: `2025-01-01T00:00:00Z..2025-01-01T00:00:00Z` }, + { input: `2025-05..2025-06`, expected: `2025-05-01T00:00:00Z..2025-06-01T00:00:00Z` }, + { input: `2025..2025-06`, expected: `2025-01-01T00:00:00Z..2025-06-01T00:00:00Z` }, + { input: `2025-05-14..2025-05-30`, expected: `2025-05-14T00:00:00Z..2025-05-30T00:00:00Z`, /* isDate: true*/ }, // + { input: `2025-05-14T00:00:00.000Z`, expected: `2025-05-14T00:00:00Z..2025-05-14T00:00:00Z` }, + { input: `2025-05-14T02:00:00.001Z`, expected: `2025-05-14T02:00:00.001Z..2025-05-14T02:00:00.001Z` }, + { input: `2025-05-14T02:45:55.003Z..2025-05-14T08:45:55.003Z`, expected: `2025-05-14T02:45:55.003Z..2025-05-14T08:45:55.003Z`, isDateRange: true }, + { input: `2025-05-14T00:00:00-05:30`, expected: `2025-05-14T05:30:00Z..2025-05-14T05:30:00Z` }, // half hour time offset + { input: `2025-09-18T12:00:00+05:00..2025-09-20T00:00:00+05:00`, expected: `2025-09-18T07:00:00Z..2025-09-19T19:00:00Z` }, + + // invalid date range specifications (exception is true) + { input: `2025-02-29`, exception: /Day out of range/ }, // not a leap year + { input: '', exception: /Invalid calendar date format/ }, + { input: ' ', exception: /Invalid calendar date format/ }, + { input: 'T12:00:00:00.000Z', exception: /Invalid ISO‑8601 datetime format/ }, + { input: '12:00:00:00.000Z', exception: /Invalid calendar date format/ }, + { input: '4/1/2023', exception: /Invalid calendar date format/ }, + ]; + + if (!IsoDatetimeRange.supportDotDotDelimitersOnly) { + testCases.push({ input: `2025...2025`, expected: `2025-01-01T00:00:00Z..2025-01-01T00:00:00Z` }); + testCases.push({ input: `2025-05-14/2025-05-15`, expected: `2025-05-14T00:00:00Z..2025-05-15T00:00:00Z` }); + } + + testCases.forEach(({ input, expected, exception, isDate = false, isDateRange = false }) => { + it(`'${input}' should parse to ${JSON.stringify(expected)}`, () => { + if (exception) { + expect(() => IsoDatetimeRange.parse(input)).toThrow(exception); + } + else { + const range = IsoDatetimeRange.parse(input); + if (input) { + if (input.indexOf('..') >= 0) { + expect(isIsoDatetimeRange(input)).toBeTruthy(); + } + else if (isIsoDatetime(input)) { + expect(isIsoDatetime(input)).toBeTruthy(); + } + else { + expect(isValidIsoDate(input)).toBeTruthy(); + } + } + if (range) { + expect(range.toString()).toEqual(expected); + } + if (isDate) { + expect(isValidIsoDate(input as string)).toBeTruthy(); + expect(isIsoDatetimeRange(input as string)).toBeFalsy(); + } + if (isDateRange) { + expect(isValidIsoDate(input as string)).toBeFalsy(); + expect(isIsoDatetimeRange(input as string)).toBeTruthy(); + } + } + }); + }); +}); \ No newline at end of file diff --git a/src/common/IsoDate/IsoDatetimeRange.ts b/src/common/IsoDate/IsoDatetimeRange.ts new file mode 100644 index 0000000..9afd0f3 --- /dev/null +++ b/src/common/IsoDate/IsoDatetimeRange.ts @@ -0,0 +1,199 @@ +import { IsoDate, isValidIsoDate } from './IsoDate.js'; +import { IsoDatetime, toIsoDatetime, isIsoDatetime } from './IsoDatetime.js'; + +/** + * IsoDatetimeRange – represents a closed interval between two ISO‑8601 datetimes. + * + * Supported input formats (single string): + * - YYYY + * - YYYY‑MM + * - YYYY‑MM‑DD + * - Full ISO‑8601 datetime strings with optional time components: + * YYYY‑MM‑DDTHH:MM:SS[.sss][Z|±hh:mm] + * + * - YYYY..YYYY + * - YYYY‑MM..YYYY‑MM + * - YYYY‑MM‑DD..YYYY‑MM‑DD + * - YYYY‑MM‑DDTHH:MM:SS[.sss][Z|±hh:mm]..YYYY‑MM‑DDTHH:MM:SS[.sss][Z|±hh:mm] + * + * The two parts are separated by a literal double‑dot (“..”). The start must be + * chronologically less‑than or equal to the end; otherwise an Error is thrown. + * + * The class re‑uses {@link IsoDatetime} for parsing and validation – no duplicate + * parsing logic is introduced. + */ +export class IsoDatetimeRange { + /** The inclusive start of the range. */ + public readonly start: IsoDatetime; + /** The inclusive end of the range. */ + public readonly end: IsoDatetime; + + /** + * flag to specify if we only support the '..' notation to delimit a date range + * Note that this class is written to support '..', '...', and the ISO 8601 standard '/', + * but in most cases, '..' is the most readable and most easily explained in an FAQ, + * so this defaults to true + * @todo hk: currently, no code has been written to change this. The easiest way is to use + * AppConfig, or if absolutely necessary, environment variables + */ + public static readonly supportDotDotDelimitersOnly = true + + private constructor(start: IsoDatetime, end: IsoDatetime) { + this.start = start; + this.end = end; + } + + /** + * Parse a range string into an {@link IsoDatetimeRange}. + * + * @param value A string containing two ISO‑8601 datetime representations + * separated by “..”. + * @throws Error if the format is invalid or the start is after the end. + */ + public static parse(value: string | undefined): IsoDatetimeRange | undefined { + let delimiters = ['..']; + if (!IsoDatetimeRange.supportDotDotDelimitersOnly) { + delimiters.push('/'); + delimiters.push('...'); + } + let range: IsoDatetimeRange = undefined; + let done = false; + for (let i = 0; (range === undefined) && (i < delimiters.length); i++) { + let delimiter = delimiters[i]; + try { + range = this._parse(value, delimiter); + } + catch (e) { + throw e; + } + }; + console.log(`*****range: ${range.toString()}`); + return range; + } + + /** + * Parse a range string into an {@link IsoDatetimeRange}. + * + * @param value A string containing two ISO‑8601 datetime representations + * separated by “..”. + * @throws Error if the format is invalid or the start is after the end. + */ + private static _parse(value: string, delimiter: string): IsoDatetimeRange { + console.log(`delimiter: ${delimiter}`); + let parts = value.split(delimiter); + if (parts.length === 1) { + parts = [value, value]; + } + if (parts.length !== 2) { + throw new Error( + `Invalid range format: "${value}". Expected exactly one "${delimiter}" separator.`, + ); + } + + const [rawStart, rawEnd] = parts.map((s) => s.trim()); + console.log(`raw: ${rawStart} .. ${rawEnd}`) + + // Helper: if the part does not contain a time separator “T”, treat it as a date + // and convert it to a midnight UTC IsoDatetime via IsoDate → IsoDatetime. + const parsePart = (part: string): IsoDatetime => { + if (part.includes('T')) { + // Full datetime – let IsoDatetime handle validation. + return IsoDatetime.parse(part); + } else { + // Date only – use IsoDate then convert. + const date = IsoDate.parse(part); + return toIsoDatetime(date); + } + }; + + const start = parsePart(rawStart); + const end = parsePart(rawEnd); + + // Ensure chronological order. + const startMs = Date.UTC( + start.year, + start.month - 1, + start.day, + start.hour, + start.minute, + start.second, + start.millisecond, + ); + const endMs = Date.UTC( + end.year, + end.month - 1, + end.day, + end.hour, + end.minute, + end.second, + end.millisecond, + ); + + if (startMs > endMs) { + throw new Error( + `Invalid range: start (${start.toString()}) is after end (${end.toString()})`, + ); + } + + console.log(`start: ${start}, end: ${end}`) + return new IsoDatetimeRange(start, end); + } + + /** Return the start datetime. */ + public getStart(): IsoDatetime { + return this.start; + } + + /** Return the end datetime. */ + public getEnd(): IsoDatetime { + return this.end; + } + + /** Return the range as a two‑element array `[start, end]`. */ + public toArray(): [IsoDatetime, IsoDatetime] { + return [this.start, this.end]; + } + + /** Normalized string representation: “start..end”. */ + public toString(): string { + return `${this.start.toString()}..${this.end.toString()}`; + } + +} + + +/** local util used by isIsoDatetimeRange to determine if any delimiters + * passed in will make text a IsoDatetimeRange + * @param delimiter any string for a delimiter + * @param text the user input string + */ +function _isIsoDatetimeRange(delimiter: string, text: string): boolean { + let dates = text.split(delimiter); + if (dates.length !== 2) { + return false; + } + if ((isIsoDatetime(dates[0]) || isValidIsoDate(dates[0])) + && (isIsoDatetime(dates[1]) || isValidIsoDate(dates[1]))) { + return true; + } + else { + return false; + } +} + +/** Returns true iff text can be converted into an IsoDatetimeRange object + * @param input + * @returns true iff text can be converted into an IsoDatetime object +*/ +export function isIsoDatetimeRange(input: string): boolean { + let delimiters = ['..']; + if (!IsoDatetimeRange.supportDotDotDelimitersOnly) { + delimiters.push('/'); + delimiters.push('...'); + } + let retval = false; + for (let i = 0; (!retval) && (i < delimiters.length); i++) { + retval = _isIsoDatetimeRange(delimiters[i], input); + } + return retval; +} \ No newline at end of file diff --git a/src/core/DeltaFs.ts b/src/core/DeltaFs.ts index e12f9fd..10ab9e2 100644 --- a/src/core/DeltaFs.ts +++ b/src/core/DeltaFs.ts @@ -14,7 +14,7 @@ import { CveId } from '../cve/CveCorePlus.js'; import { DeltaOutputItem, Delta } from './Delta.js'; import { FsReader } from '../adapters/fs/FsReader.js'; -export type IsoDate = string; // @todo make a better class +// export type IsoDate = string; // @todo make a better class // export type CveId = string; // @todo make a better class diff --git a/src/search/BasicSearchManager.ts b/src/search/BasicSearchManager.ts index fbb3251..339d18d 100644 --- a/src/search/BasicSearchManager.ts +++ b/src/search/BasicSearchManager.ts @@ -53,6 +53,7 @@ export class BasicSearchManager { async search(searchText: string, options: Partial = undefined): Promise { let response = undefined; const builder = new SearchQueryBuilder(searchText, options); + // console.log(`search: ${JSON.stringify(builder._searchText, null, 2)}`) const result: CveResult = builder.buildQuery() // console.log(`query body (q)=${JSON.stringify(result.data['q'], null, 2)}`) if (result.isOk()) { diff --git a/src/search/SearchExpression.ts b/src/search/SearchExpression.ts new file mode 100644 index 0000000..8ba5f69 --- /dev/null +++ b/src/search/SearchExpression.ts @@ -0,0 +1,50 @@ +import { IsoDate } from '../common/IsoDate/IsoDate.js'; +import { IsoDatetime } from '../common/IsoDate/IsoDatetime.js'; +import { IsoDatetimeRange } from '../common/IsoDate/IsoDatetimeRange.js' + +import { SearchRequestTypeId } from './SearchRequest.js'; + + +type SearchStringExpression = string; +// type SearchDateExpression = IsoDate; +type SearchDateRangeExpression = IsoDatetimeRange + +/** SearchExpression is the user's search string that has been tokenized, analyzed, cateogrized, and decorated + */ +export class SearchExpression { + + /** the part of the original user string after SearchRequest.tokenizeSearchText(), + * each tokenized string will be converted into one SearchExpression + */ + private _userString: string; + public get userString(): string { + return this._userString; + } + public set userString(value: string) { + this._userString = value; + } + + /** the processed expression */ + private _expression: SearchStringExpression | SearchDateRangeExpression; + public get expression(): SearchStringExpression | SearchDateRangeExpression { + return this._expression; + } + public set expression(value: SearchStringExpression | SearchDateRangeExpression) { + this._expression = value; + } + + /** the Expression type */ + private _typeId: SearchRequestTypeId; + public get typeId(): SearchRequestTypeId { + return this._typeId; + } + public set typeId(value: SearchRequestTypeId) { + this._typeId = value; + } + + constructor(userString?: string, expression?: string, typeId?: SearchRequestTypeId) { + this.userString = userString; + this.expression = expression; + this._typeId = typeId; + } +} \ No newline at end of file diff --git a/src/search/SearchQueryBuilder.test.unit.ts b/src/search/SearchQueryBuilder.test.unit.ts index 03f289e..51f0be8 100644 --- a/src/search/SearchQueryBuilder.test.unit.ts +++ b/src/search/SearchQueryBuilder.test.unit.ts @@ -112,6 +112,7 @@ describe(`SearchQueryBuilder`, () => { }], [`CAPEC-64`, { default_operator: 'OR' }], [`2023-12-21`, { track_total_hits: true }], + [`2024-11-27T09:00:00.000Z..2024-12-01T00:00:00.000Z`, { track_total_hits: true }], ] testCases.forEach((test: [string, Partial]) => { it(`(${test[0]},${JSON.stringify(test[1])})..buildQuery() correctly returns the expected query`, async () => { diff --git a/src/search/SearchQueryBuilder.ts b/src/search/SearchQueryBuilder.ts index 87f98e9..7561f1f 100644 --- a/src/search/SearchQueryBuilder.ts +++ b/src/search/SearchQueryBuilder.ts @@ -4,6 +4,7 @@ import { IsoDatetime } from '../common/IsoDate/IsoDatetime.js'; import { toSearchDateDslString } from '../adapters/search/OpensearchDatetimeUtils.js'; import { SearchOptions } from './BasicSearchManager.js'; import { SearchRequest } from './SearchRequest.js'; +import { IsoDatetimeRange } from '../common/IsoDate/IsoDatetimeRange.js'; /** @@ -69,7 +70,7 @@ export class SearchQueryBuilder { /** builds the proper query for openSearch */ buildQuery(): CveResult { const result: CveResult = this._searchRequest.processSearchText(); - // console.log(`result.data: ${JSON.stringify(result.data)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) // if there are any errors, return result which already contains errors and notes if (!result.isOk()) { @@ -83,21 +84,44 @@ export class SearchQueryBuilder { // right now, we only handle 2 types of queries: // 1. date/date ranges // 2. query_string for everything else + // @todo: the following logic will be much simplified when we use searchExpressions + // instead of strings and have to check if the searchText is a date, range, or string const isDate = SearchRequest.isDateString(this._searchText); - // console.log(`isDate(): ${isDate}`) - if (isDate) { + const isDateRange = SearchRequest.isDateRangeString(this._searchText); + // console.log(`isDate(): ${isDate}`); + // console.log(`isDateRange(): ${isDateRange}`); + let startDate; + let stopDate; + if (isDate || isDateRange) { + // we currently support 3 use cases only: + // 1. YYYY-MM-DD + // 2. YYYY-MM-DDTHH:MM:SS(.mmmm)(Z) + // 3. YYYY-MM-DD..YYYY-MM-DD + // 4. YYYY-MM-DDTHH:MM:SS(.mmmm)(Z)..YYYY-MM-DDTHH:MM:SS(.mmmm)(Z) + if (isDate) { + // case 1: YYYY-MM-DD + // case 2: YYYY-MM-DDTHH:MM:SS(.mmmm)(Z) + // if the user had requested an IsoDate, it should stay as an IsoDate and not be cast to an IsoDatetime + // otherwise YYYY or YYYY-MM requests would not work + startDate = (this._searchText.length > 10) ? IsoDatetime.parse(this._searchText) : IsoDate.parse(this._searchText); + // we are now doing inclusive date ranges, so the same startDate and stopDate should now be used + stopDate = startDate; //IsoDatetime.fromIsoDate(startDate).getNextDay(); + } else if (isDateRange) { + // case 3: YYYY-MM-DD..YYYY-MM-DD + // case 4: YYYY-MM-DDTHH:MM:SS(.mmmm)(Z)..YYYY-MM-DDTHH:MM:SS(.mmmm)(Z) + const dateRange = IsoDatetimeRange.parse(this._searchText); + startDate = dateRange.start; + stopDate = dateRange.end; + } + // console.log(`buildQuery(): startDate: ${startDate}, stopDate: ${stopDate}`); // assemble all the date fields into an array let dateFields = []; - // if the user had requested an IsoDate, it should stay as an IsoDate and not be cast to an IsoDatetime - // otherwise YYYY or YYYY-MM requests would not work - const startDate = (this._searchText.length > 10) ? IsoDatetime.parse(this._searchText) : IsoDate.parse(this._searchText); - const stopDate = IsoDatetime.fromIsoDate(startDate).getNextDay() SearchQueryBuilder.kDateFieldPaths.map(path => { let field = `{ "range": { "${path}": { "gte": "${toSearchDateDslString(startDate)}", - "lt": "${toSearchDateDslString(stopDate)}" + "lte": "${toSearchDateDslString(stopDate)}" } } }`; diff --git a/src/search/SearchRequest.test.unit.ts b/src/search/SearchRequest.test.unit.ts index f4d1fac..b6b27cf 100644 --- a/src/search/SearchRequest.test.unit.ts +++ b/src/search/SearchRequest.test.unit.ts @@ -1,7 +1,8 @@ // For a more comprehensive set of test cases, see the tests // in test_cases/search_* -import { describe, it, expect } from '@jest/globals'; +import { describe, it, test, expect } from '@jest/globals'; import { SearchOptions } from "./BasicSearchManager.js" +import { CveResult } from '../result/CveResult.js'; import { SearchRequestType, SearchRequest, SearchRequestTypeId } from "./SearchRequest.js"; describe(`SearchRequest`, () => { @@ -37,7 +38,7 @@ describe(`SearchRequest`, () => { expect(result.hasErrors()).toBeTruthy(); expect(result.hasNotes()).toBeTruthy(); expect(result.data['searchTextType']).toBe("SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS"); - expect(result.data['processedSearchText']).toBe(kSimpleUnsupportedSearchString); + expect(result.data['processedSearchText']).toBe(''); expect(result).toMatchSnapshot(); expect(req._searchText).toBe(result.data['processedSearchText']); expect(req).toMatchSnapshot(); @@ -176,9 +177,9 @@ describe(`SearchRequest`, () => { [ // ----- reserved characters in strings ----- - ["{getTotal()}", 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS', "{getTotal()}"], - ["{=getTotal()}", 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS', "{=getTotal()}"], - ["{inField('description')}", 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS', "{inField('description')}"], + ["{getTotal()}", 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS', ''], + ["{=getTotal()}", 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS', ''], + ["{inField('description')}", 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS', ''], // ["CVE-2020-0001 {inField('description')}", 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS', `\"CVE-2020-0001\\\" {inField('description')}"], // ----- disallowed strings ----- // ["CVE–1999–0001", 'SEARCH_GENERAL_TEXT', "CVE–1999–0001"], @@ -261,9 +262,9 @@ describe(`SearchRequest`, () => { // ["2001:db8:3333:4444:5555:6666:1.2.3.4", 'SEARCH_AS_IPv6', `\"2001:db8:3333:4444:5555:6666:1.2.3.4\"`], // ["::11.22.33.44", 'SEARCH_AS_IPv6', `\"::11.22.33.44\"`], // ["2001:0000:1234:0000:0000:C1C0:ABCD:0876", 'SEARCH_AS_IPv6', `\"2001:0000:1234:0000:0000:C1C0:ABCD:0876\"`], - ["FF02:0000:0000:0000:0000:0000:0000:0000:0001", 'SEARCH_STRING_NOT_SUPPORTED', "FF02:0000:0000:0000:0000:0000:0000:0000:0001"], - ["3ffe:b00::1::a", 'SEARCH_STRING_NOT_SUPPORTED', "3ffe:b00::1::a"], - [":", 'SEARCH_STRING_NOT_SUPPORTED', ":"], + ["FF02:0000:0000:0000:0000:0000:0000:0000:0001", 'SEARCH_STRING_NOT_SUPPORTED', ''], + ["3ffe:b00::1::a", 'SEARCH_STRING_NOT_SUPPORTED', ''], + [":", 'SEARCH_STRING_NOT_SUPPORTED', ''], // // ----- version search strings ----- // ["1.0", 'SEARCH_AS_VERSION', `\"1.0\"`], // ["1.0.1", 'SEARCH_AS_VERSION', `\"1.0.1\"`], @@ -297,6 +298,17 @@ describe(`SearchRequest`, () => { // ["01.102.103.104", 'SEARCH_AS_VERSION', `\"01.102.103.104\"`], // looks like ipv4, but not because of first 0 // ["127.0.0.1.1.1.1", 'SEARCH_AS_VERSION', `\"127.0.0.1.1.1.1\"`], // [".127.0.0", 'SEARCH_STRING_NOT_SUPPORTED', ".127.0.0"], // @todo + + // [`2025`, 'SEARCH_DATE', `2025-01-01..2026-01-01`], + // [`2025-01`, 'SEARCH_DATE', `2025-01-01..2025-02-01`], + // [`2025-01-01`, 'SEARCH_DATE', `2025-01-01..2025-01-02`], + // [`2025-01-01T20:00:00Z`, 'SEARCH_DATE', `2025-01-01T20:00:00Z..2025-01-01T20:00:01Z`], + // [`2025-01-01T20:00:00.000Z`, 'SEARCH_DATE', `2025-01-01T20:00:00.000Z..2025-01-01T20:00:00.001Z`], + // [`2025-01-01T20:00:00.000`, 'SEARCH_DATE', `2025-01-01T20:00:00.000Z..2025-01-01T20:00:00.001Z`], + // [`2025-01..2025-02`, 'SEARCH_DATE_RANGE', `2025-01-01..2025-03-01`], + // [`2025-01-01..2025-01-02`, 'SEARCH_DATE_RANGE', `2025-01-01..2025-01-02`], + [`2025-01-01T20:00:00.000Z..2025-01-01T20:00:00.001Z`, 'SEARCH_DATE_RANGE', `2025-01-01T20:00:00Z..2025-01-01T20:00:00.001Z`], + [`2025-01-01T20:00:00.000..2025-01-01T20:00:00.001`, 'SEARCH_DATE_RANGE', `2025-01-01T20:00:00Z..2025-01-01T20:00:00.001Z`], ] .forEach((test: [string, string, string]) => { it(`processSearchText("${test[0]}") correctly returns ${test[1]} and errors`, async () => { @@ -423,3 +435,56 @@ describe(`SearchRequest`, () => { }); }); +describe(`isDateString()`, () => { + const tests: Array<{ input: string, expected: boolean; }> = [ + { input: "2024-10-01T00:00:00.000Z", expected: true }, + { input: "2024-10-01T00:00:00.000", expected: true }, + { input: "2024-10-01T00:00:00Z", expected: true }, + { input: "2024-10-01T00:00:00", expected: true }, + { input: "2024-10-01", expected: true }, + // { input: "2024-10", expected: true }, + // { input: "2024", expected: true }, + // invalid dates + { input: "abc", expected: false }, + { input: "2024/10/01", expected: false }, + { input: "10/01/2024", expected: false }, + { input: "10/01", expected: false }, + { input: "10", expected: false }, + ]; + + tests.forEach(({ input, expected }) => { + test(`properly returns true iff '${input}' is a valid date: ${expected}`, () => { + expect(SearchRequest.isDateString(input)).toBe(expected); + }); + }); +}); + +describe(`isDateRangeString()`, () => { + const tests: Array<{ input: string, expected: boolean; }> = [ + // { input: "2024/2025", expected: true }, + // { input: "2024-12/2025-02", expected: true }, + // { input: "2024-10-01/2024-10-02", expected: true }, + { input: "2024-10-01..2024-10-02", expected: true }, + // { input: "2024-10-01...2024-10-02", expected: true }, + // standardizing on using .. because it is more intuitive and easier to read + { input: "2024-10-01T00:00:00.000Z..2024-10-01T00:00:00.001Z", expected: true }, + { input: "2024-10-01T00:00:00.000..2024-10-01T00:00:00.001", expected: true }, + { input: "2024-10-01T00:00:00Z..2024-10-01T00:00:00Z", expected: true }, + { input: "2024-10-01T00:00:00..2024-10-01T00:00:00", expected: true }, + { input: "2024-10-01..2024-10-01", expected: true }, + { input: "2024-10-01..2024-10-02", expected: true }, + { input: "2024-02-28..2024-02-29", expected: true }, // leap year + { input: "2024-10-01T00:00:00.000..2024-10-01T00:00:00.000", expected: true }, + // invalid date ranges + { input: "2024-02-29..2024-02-30", expected: false }, // invalid leap year + { input: "2/1/2022..2/2/2022", expected: false }, // invalid date format + { input: "2024-10-01T00:00:00.000Z", expected: false }, + { input: "2024", expected: false }, + ]; + + tests.forEach(({ input, expected }) => { + test(`properly returns true iff '${input}' is a valid date range: ${expected}`, () => { + expect(SearchRequest.isDateRangeString(input)).toBe(expected); + }); + }); +}); diff --git a/src/search/SearchRequest.ts b/src/search/SearchRequest.ts index 4403a6b..d2d4166 100644 --- a/src/search/SearchRequest.ts +++ b/src/search/SearchRequest.ts @@ -4,7 +4,10 @@ import validator from 'validator'; // import { CveId } from "../CveId.js" import { CveResult, CveErrorId } from "../result/CveResult.js"; import { SearchOptions } from "./BasicSearchManager.js" -import { SearchToken } from './SearchToken.js'; +import { SearchExpression } from './SearchExpression.js'; +import { IsoDate } from '../common/IsoDate/IsoDate.js'; +import { IsoDatetime, isIsoDatetime } from '../common/IsoDate/IsoDatetime.js'; +import { IsoDatetimeRange, isIsoDatetimeRange } from '../common/IsoDate/IsoDatetimeRange.js'; export const SearchRequestType = { @@ -120,15 +123,18 @@ export class SearchRequest { } else { // general search text processing + let expression = new SearchExpression() this._searchText = this._searchText?.trim(); const cleanedSearchText = SearchRequest.replaceRepeatingSymbols(this._searchText); const text: string[] = SearchRequest.tokenizeSearchText(cleanedSearchText); let overallType: SearchRequestTypeId = 'SEARCH_STRING_UNKNOWN_TYPE'; let errorIds: CveErrorId[] = [] let newSearchText = '' - let searchTokens: SearchToken[] = [] + let searchExpressions: SearchExpression[] = [] text.forEach(token => { let type = SearchRequest.findSearchRequestType(token); + expression.userString = token; + expression.typeId = type // post-processing based on type switch (type) { case 'SEARCH_AS_CVE_ID': @@ -141,22 +147,29 @@ export class SearchRequest { case 'SEARCH_AS_VERSION': case 'SEARCH_AS_FILESPEC': case 'SEARCH_PHRASE': - // token = `\\\"${token}\\\"` // this is what is needed for mccoy, but when using ``, it is not necessary - token = `\"${token}\"` + expression.expression = `\"${token}\"` result = CveResult.ok({}, [SearchRequestType[type]]); break; + case 'SEARCH_AS_WILDCARD_QUESTION': case 'SEARCH_AS_WILDCARD_ASTERISK': case 'SEARCH_GENERAL_TEXT': - token = token.replaceAll(`"`, ''); + expression.expression = token.replaceAll(`"`, ''); result = CveResult.ok({}, [SearchRequestType[type]]); // result.pushNotes('all quotes have been removed from search text'); break; + case 'SEARCH_DATE': + case 'SEARCH_DATE_RANGE': + result = CveResult.ok({}, [SearchRequestType[type]]); + expression.expression = IsoDatetimeRange.parse(token); + break; case 'SEARCH_STRING_NOT_SUPPORTED': // case 'WILDCARD_ASTERISK_SEARCH_NOT_SUPPORTED': result = CveResult.error(9003, [SearchRequestType[type]]); + expression.expression = ''; break; case 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS': result = CveResult.error(9004, [SearchRequestType[type]]); + expression.expression = ''; break; default: result = CveResult.ok({}, [SearchRequestType[type]]); @@ -164,8 +177,7 @@ export class SearchRequest { } // token at this point has been properly decorated ready to be used // in opensearch - let searchToken = new SearchToken(token, type); - newSearchText = `${newSearchText} ${token}` + newSearchText = `${newSearchText} ${expression.expression}` if (overallType === 'SEARCH_STRING_UNKNOWN_TYPE') { overallType = type; } @@ -176,6 +188,7 @@ export class SearchRequest { this._searchText = newSearchText.trim() result.data = { searchTextType: overallType, + expression, processedSearchText: this._searchText }; // errorIds.forEach(id => { @@ -222,15 +235,17 @@ export class SearchRequest { } + /** returns true iff searchText is a IsoDate or IsoDatetime */ static isDateString(searchText: string | null): boolean { // performance-optimized and safer version of CVE Schema 5.1 regex for dates generated by gemini 2.5 pro - const regex = /^((?:(?:2000|2400|2800)|(?:(?:19|2\d)(?:0[48]|[2468][048]|[13579][26])))-02-29|(?:(?:19|2\d)\d{2})-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|02-(?:0[1-9]|1\d|2[0-8])))$/; - if (!searchText || typeof searchText !== 'string' || searchText.length <= 0) { - return false; - } - else { - return regex.test(searchText); - } + // const regex = /^((?:(?:2000|2400|2800)|(?:(?:19|2\d)(?:0[48]|[2468][048]|[13579][26])))-02-29|(?:(?:19|2\d)\d{2})-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|02-(?:0[1-9]|1\d|2[0-8])))$/; + return isIsoDatetime(searchText) + } + + + /** returns true iff searchText is a IsoDate or IsoDatetime range */ + static isDateRangeString(searchText: string | null): boolean { + return isIsoDatetimeRange(searchText) } @@ -454,15 +469,15 @@ export class SearchRequest { if (searchText.includes('{')) { return 'SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS' } - // else if (searchText[0] == '"' && searchText[searchText.length - 1] == '"') { - // else if (new RegExp(/"([^"]*)"/g).test(searchText)) { - // else if (new RegExp(/"(?:[^"\\]|\\.)+"|[^\s,]+/g).test(searchText)) { else if (SearchRequest.isQuotedString(searchText)) { return 'SEARCH_PHRASE'; } else if (SearchRequest.isDateString(searchText)) { return 'SEARCH_DATE'; } + else if (SearchRequest.isDateRangeString(searchText)) { + return 'SEARCH_DATE_RANGE'; + } else if (searchText.includes('*')) { return 'SEARCH_AS_WILDCARD_ASTERISK' } diff --git a/src/search/SearchToken.ts b/src/search/SearchToken.ts deleted file mode 100644 index b2c934b..0000000 --- a/src/search/SearchToken.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SearchRequestTypeId } from './SearchRequest.js'; - -/** SearchToken is the user's search string that has been tokenized, analyzed, cateogrized, and decorated - */ -export class SearchToken { - - /** the processed text string */ - private _text: string; - private set text(value: string) { - this._text = value; - } - public get text(): string { - return this._text; - } - - /** the token type */ - private _typeId: SearchRequestTypeId; - public get typeId(): SearchRequestTypeId { - return this._typeId; - } - public set typeId(value: SearchRequestTypeId) { - this._typeId = value; - } - - - constructor(text: string, typeId: SearchRequestTypeId) { - this._text = text - this._typeId = typeId - } -} \ No newline at end of file diff --git a/src/search/__snapshots__/BasicSearchManager.test.e2e.ts.snap b/src/search/__snapshots__/BasicSearchManager.test.e2e.ts.snap index 5575f70..c0e9227 100644 --- a/src/search/__snapshots__/BasicSearchManager.test.e2e.ts.snap +++ b/src/search/__snapshots__/BasicSearchManager.test.e2e.ts.snap @@ -204,7 +204,12 @@ CveResult { exports[`BasicSearchManager (e2e) search({{funct()}}) correctly returns expected errors (error CveResult) 1`] = ` CveResult { "data": Object { - "processedSearchText": "{{funct()}}", + "expression": SearchExpression { + "_expression": "", + "_typeId": "SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS", + "_userString": "{{funct()}}", + }, + "processedSearchText": "", "searchTextType": "SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS", }, "errors": Array [ diff --git a/src/search/__snapshots__/SearchQueryBuilder.test.unit.ts.snap b/src/search/__snapshots__/SearchQueryBuilder.test.unit.ts.snap index bcea759..8c55148 100644 --- a/src/search/__snapshots__/SearchQueryBuilder.test.unit.ts.snap +++ b/src/search/__snapshots__/SearchQueryBuilder.test.unit.ts.snap @@ -3,6 +3,11 @@ exports[`SearchQueryBuilder ("https://pastebin.com/kpzHKKJu",{"track_total_hits":true})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "\\"https://pastebin.com/kpzHKKJu\\"", + "_typeId": "SEARCH_AS_URL", + "_userString": "https://pastebin.com/kpzHKKJu", + }, "processedSearchText": "\\"https://pastebin.com/kpzHKKJu\\"", "q": Object { "from": 0, @@ -35,6 +40,11 @@ CveResult { exports[`SearchQueryBuilder ("microsoft office",{"track_total_hits":false})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "microsoft office", + "_typeId": "SEARCH_GENERAL_TEXT", + "_userString": "microsoft office", + }, "processedSearchText": "microsoft office", "q": Object { "from": 0, @@ -66,6 +76,11 @@ CveResult { exports[`SearchQueryBuilder ("office",{"track_total_hits":true})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "office", + "_typeId": "SEARCH_GENERAL_TEXT", + "_userString": "office", + }, "processedSearchText": "office", "q": Object { "from": 0, @@ -98,17 +113,84 @@ CveResult { exports[`SearchQueryBuilder (2023-12-21,{"track_total_hits":true})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { - "processedSearchText": "2023-12-21", + "expression": SearchExpression { + "_expression": IsoDatetimeRange { + "end": IsoDatetime { + "_parsedValue": "2023-12-21", + "day": 21, + "hour": 0, + "millisecond": 0, + "minute": 0, + "month": 12, + "second": 0, + "year": 2023, + }, + "start": IsoDatetime { + "_parsedValue": "2023-12-21", + "day": 21, + "hour": 0, + "millisecond": 0, + "minute": 0, + "month": 12, + "second": 0, + "year": 2023, + }, + }, + "_typeId": "SEARCH_DATE", + "_userString": "2023-12-21", + }, + "processedSearchText": "2023-12-21T00:00:00Z..2023-12-21T00:00:00Z", "q": Object { "from": 0, "query": Object { "bool": Object { "minimum_should_match": 1, "should": Array [ + Object { + "range": Object { + "cveMetadata.datePublished": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, + Object { + "range": Object { + "cveMetadata.dateRejected": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, + Object { + "range": Object { + "cveMetadata.dateReserved": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, + Object { + "range": Object { + "cveMetadata.dateUpdated": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, + Object { + "range": Object { + "containers.cna.datePublic": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, Object { "range": Object { "containers.cna.providerMetadata.dateUpdated": Object { "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", }, }, }, @@ -116,6 +198,31 @@ CveResult { "range": Object { "containers.cna.timeline.time": Object { "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, + Object { + "range": Object { + "containers.adp.metrics.other.content.dateAdded": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, + Object { + "range": Object { + "containers.adp.metrics.other.content.timestamp": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", + }, + }, + }, + Object { + "range": Object { + "containers.adp.providerMetadata.dateUpdated": Object { + "gte": "2023-12-21||/d", + "lte": "2023-12-21||/d", }, }, }, @@ -142,9 +249,153 @@ CveResult { } `; +exports[`SearchQueryBuilder (2024-11-27T09:00:00.000Z..2024-12-01T00:00:00.000Z,{"track_total_hits":true})..buildQuery() correctly returns the expected query 1`] = ` +CveResult { + "data": Object { + "expression": SearchExpression { + "_expression": IsoDatetimeRange { + "end": IsoDatetime { + "_parsedValue": "2024-12-01T00:00:00.000Z", + "day": 1, + "hour": 0, + "millisecond": 0, + "minute": 0, + "month": 12, + "second": 0, + "year": 2024, + }, + "start": IsoDatetime { + "_parsedValue": "2024-11-27T09:00:00.000Z", + "day": 27, + "hour": 9, + "millisecond": 0, + "minute": 0, + "month": 11, + "second": 0, + "year": 2024, + }, + }, + "_typeId": "SEARCH_DATE_RANGE", + "_userString": "2024-11-27T09:00:00.000Z..2024-12-01T00:00:00.000Z", + }, + "processedSearchText": "2024-11-27T09:00:00Z..2024-12-01T00:00:00Z", + "q": Object { + "from": 0, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "range": Object { + "cveMetadata.datePublished": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "cveMetadata.dateRejected": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "cveMetadata.dateReserved": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "cveMetadata.dateUpdated": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "containers.cna.datePublic": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "containers.cna.providerMetadata.dateUpdated": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "containers.cna.timeline.time": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "containers.adp.metrics.other.content.dateAdded": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "containers.adp.metrics.other.content.timestamp": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + Object { + "range": Object { + "containers.adp.providerMetadata.dateUpdated": Object { + "gte": "2024-11-27T09:00:00Z", + "lte": "2024-12-01T00:00:00Z", + }, + }, + }, + ], + }, + }, + "size": 25, + "sort": Array [ + Object { + "cveMetadata.cveId.keyword": Object { + "order": "desc", + }, + }, + ], + "track_total_hits": true, + }, + "searchTextType": "SEARCH_DATE_RANGE", + }, + "errors": undefined, + "notes": Array [ + "search text is a date range (ISO 8601)", + ], + "status": "ok", +} +`; + exports[`SearchQueryBuilder (CAPEC-64,{"default_operator":"OR"})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "\\"CAPEC-64\\"", + "_typeId": "SEARCH_AS_CAPEC_ID", + "_userString": "CAPEC-64", + }, "processedSearchText": "\\"CAPEC-64\\"", "q": Object { "from": 0, @@ -177,6 +428,11 @@ CveResult { exports[`SearchQueryBuilder (CVE-2000,{"metadataOnly":false})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "\\"CVE-2000\\"", + "_typeId": "SEARCH_AS_CVE_YEAR", + "_userString": "CVE-2000", + }, "processedSearchText": "\\"CVE-2000\\"", "q": Object { "from": 0, @@ -209,6 +465,11 @@ CveResult { exports[`SearchQueryBuilder (CVE-2020-5422,{"useCache":false})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "\\"CVE-2020-5422\\"", + "_typeId": "SEARCH_AS_CVE_ID", + "_userString": "CVE-2020-5422", + }, "processedSearchText": "\\"CVE-2020-5422\\"", "q": Object { "from": 0, @@ -241,6 +502,11 @@ CveResult { exports[`SearchQueryBuilder (CWE-123,{"track_total_hits":false,"default_operator":"OR","metadataOnly":true})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "\\"CWE-123\\"", + "_typeId": "SEARCH_AS_CWE_ID", + "_userString": "CWE-123", + }, "processedSearchText": "\\"CWE-123\\"", "q": Object { "_source": Array [ @@ -276,6 +542,11 @@ CveResult { exports[`SearchQueryBuilder (microsoft office,{"track_total_hits":false})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "office", + "_typeId": "SEARCH_GENERAL_TEXT", + "_userString": "office", + }, "processedSearchText": "microsoft office", "q": Object { "from": 0, @@ -307,6 +578,11 @@ CveResult { exports[`SearchQueryBuilder (office,{"track_total_hits":true})..buildQuery() correctly returns the expected query 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "office", + "_typeId": "SEARCH_GENERAL_TEXT", + "_userString": "office", + }, "processedSearchText": "office", "q": Object { "from": 0, @@ -339,6 +615,11 @@ CveResult { exports[`SearchQueryBuilder constructor with options for paging correctly returns the number requested 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "office", + "_typeId": "SEARCH_GENERAL_TEXT", + "_userString": "office", + }, "processedSearchText": "office", "searchTextType": "SEARCH_GENERAL_TEXT", }, diff --git a/src/search/__snapshots__/SearchRequest.test.unit.ts.snap b/src/search/__snapshots__/SearchRequest.test.unit.ts.snap index 0b0585f..c61da36 100644 --- a/src/search/__snapshots__/SearchRequest.test.unit.ts.snap +++ b/src/search/__snapshots__/SearchRequest.test.unit.ts.snap @@ -3,7 +3,12 @@ exports[`SearchRequest processSearchText() correctly returns an "errors" result 1`] = ` CveResult { "data": Object { - "processedSearchText": "{}", + "expression": SearchExpression { + "_expression": "", + "_typeId": "SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS", + "_userString": "{}", + }, + "processedSearchText": "", "searchTextType": "SEARCH_STRING_CANNOT_CONTAIN_RESERVED_CHARACTERS", }, "errors": Array [ @@ -22,13 +27,18 @@ CveResult { exports[`SearchRequest processSearchText() correctly returns an "errors" result 2`] = ` SearchRequest { - "_searchText": "{}", + "_searchText": "", } `; exports[`SearchRequest processSearchText() correctly returns an "ok" result 1`] = ` CveResult { "data": Object { + "expression": SearchExpression { + "_expression": "office", + "_typeId": "SEARCH_GENERAL_TEXT", + "_userString": "office", + }, "processedSearchText": "office", "searchTextType": "SEARCH_GENERAL_TEXT", }, diff --git a/src/search/test_cases/__snapshots__/search_dates.test.e2e.ts.snap b/src/search/test_cases/__snapshots__/search_dates.test.e2e.ts.snap index cdb0619..aed2028 100644 --- a/src/search/test_cases/__snapshots__/search_dates.test.e2e.ts.snap +++ b/src/search/test_cases/__snapshots__/search_dates.test.e2e.ts.snap @@ -1,51 +1,133 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Date Search (e2e tests) BasicSearchManager.search() search('1999') should return cves including undefined 1`] = ` +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-06-14') should return cves including CVE-2024-6006 1`] = ` Array [ - "CVE-1999-0529", + "CVE-2024-31162", + "CVE-2024-36155", + "CVE-2024-5981", + "CVE-2024-6006", ] `; -exports[`Date Search (e2e tests) BasicSearchManager.search() search('2022-01-11') should return cves including CVE-2022-21963 1`] = ` +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-11-27') should return cves including CVE-2024-9420,CVE-2024-54014,CVE-2024-11921,CVE-2024-10451,CVE-2023-35159,CVE-2023-35158,CVE-2023-35157,CVE-2023-35156,CVE-2023-34254,CVE-2023-32728 1`] = ` Array [ - "CVE-2021-45052", - "CVE-2022-21833", - "CVE-2022-21834", - "CVE-2022-21836", - "CVE-2022-21843", - "CVE-2022-21862", - "CVE-2022-21864", - "CVE-2022-21867", - "CVE-2022-21880", - "CVE-2022-21881", - "CVE-2022-21883", - "CVE-2022-21894", - "CVE-2022-21900", - "CVE-2022-21903", - "CVE-2022-21904", - "CVE-2022-21908", - "CVE-2022-21916", - "CVE-2022-21924", - "CVE-2022-21958", - "CVE-2022-21960", - "CVE-2022-21961", - "CVE-2022-21962", - "CVE-2022-21963", + "CVE-2023-25521", + "CVE-2023-32728", + "CVE-2023-34254", + "CVE-2023-35156", + "CVE-2023-35157", + "CVE-2023-35158", + "CVE-2023-35159", + "CVE-2024-10451", + "CVE-2024-11921", + "CVE-2024-54014", + "CVE-2024-9420", ] `; -exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-06-14') should return cves including CVE-2024-6006 1`] = ` +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-11-27..2024-11-27') should return cves including CVE-2024-9420,CVE-2024-54014,CVE-2024-11921,CVE-2024-10451,CVE-2023-35159,CVE-2023-35158,CVE-2023-35157,CVE-2023-35156,CVE-2023-34254,CVE-2023-32728 1`] = ` Array [ - "CVE-2024-31162", - "CVE-2024-36155", - "CVE-2024-5981", - "CVE-2024-6006", + "CVE-2023-25521", + "CVE-2023-32728", + "CVE-2023-34254", + "CVE-2023-35156", + "CVE-2023-35157", + "CVE-2023-35158", + "CVE-2023-35159", + "CVE-2024-10451", + "CVE-2024-11921", + "CVE-2024-54014", + "CVE-2024-9420", +] +`; + +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-11-27T00:00:00..2024-11-27T23:59:59') should return cves including CVE-2024-9420,CVE-2024-54014,CVE-2024-11921,CVE-2024-10451,CVE-2023-35159,CVE-2023-35158,CVE-2023-35157,CVE-2023-35156,CVE-2023-34254,CVE-2023-32728 1`] = ` +Array [ + "CVE-2023-25521", + "CVE-2023-32728", + "CVE-2023-34254", + "CVE-2023-35156", + "CVE-2023-35157", + "CVE-2023-35158", + "CVE-2023-35159", + "CVE-2024-10451", + "CVE-2024-11921", + "CVE-2024-54014", + "CVE-2024-9420", +] +`; + +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-11-27T00:00:00.000..2024-11-27T23:59:59.999') should return cves including CVE-2024-9420 1`] = ` +Array [ + "CVE-2023-25521", + "CVE-2023-32728", + "CVE-2023-34254", + "CVE-2023-35156", + "CVE-2023-35157", + "CVE-2023-35158", + "CVE-2023-35159", + "CVE-2024-10451", + "CVE-2024-11921", + "CVE-2024-54014", + "CVE-2024-9420", +] +`; + +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-11-27T00:00:00.000Z..2024-11-27T23:59:59.999Z') should return cves including CVE-2024-9420,CVE-2024-54014,CVE-2024-11921,CVE-2024-10451,CVE-2023-35159,CVE-2023-35158,CVE-2023-35157,CVE-2023-35156,CVE-2023-34254,CVE-2023-32728 1`] = ` +Array [ + "CVE-2023-25521", + "CVE-2023-32728", + "CVE-2023-34254", + "CVE-2023-35156", + "CVE-2023-35157", + "CVE-2023-35158", + "CVE-2023-35159", + "CVE-2024-10451", + "CVE-2024-11921", + "CVE-2024-54014", + "CVE-2024-9420", +] +`; + +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-11-27T00:00:00Z..2024-11-27T23:59:59Z') should return cves including CVE-2024-9420,CVE-2024-54014,CVE-2024-11921,CVE-2024-10451,CVE-2023-35159,CVE-2023-35158,CVE-2023-35157,CVE-2023-35156,CVE-2023-34254,CVE-2023-32728 1`] = ` +Array [ + "CVE-2023-25521", + "CVE-2023-32728", + "CVE-2023-34254", + "CVE-2023-35156", + "CVE-2023-35157", + "CVE-2023-35158", + "CVE-2023-35159", + "CVE-2024-10451", + "CVE-2024-11921", + "CVE-2024-54014", + "CVE-2024-9420", +] +`; + +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2024-11-27T09:00:00.000Z..2024-11-28T00:00:00.000Z') should return cves including CVE-2024-9420 1`] = ` +Array [ + "CVE-2023-25521", + "CVE-2023-32728", + "CVE-2023-34254", + "CVE-2023-35156", + "CVE-2023-35157", + "CVE-2023-35158", + "CVE-2023-35159", + "CVE-2024-10451", + "CVE-2024-11921", + "CVE-2024-9420", +] +`; + +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2025-01-11T02:31:22') should return cves including CVE-2024-42169 1`] = ` +Array [ + "CVE-2024-42169", ] `; -exports[`Date Search (e2e tests) BasicSearchManager.search() search('2025-01-11') should return cves including CVE-2024-42169 1`] = ` +exports[`Date Search (e2e tests) BasicSearchManager.search() search('2025-01-11T02:31:22.611Z') should return cves including CVE-2024-42169 1`] = ` Array [ "CVE-2024-42169", - "CVE-2025-23114", ] `; diff --git a/src/search/test_cases/search_dates.test.e2e.ts b/src/search/test_cases/search_dates.test.e2e.ts index f69522d..8fd1ed5 100644 --- a/src/search/test_cases/search_dates.test.e2e.ts +++ b/src/search/test_cases/search_dates.test.e2e.ts @@ -12,21 +12,42 @@ describe('Date Search (e2e tests)', () => { describe.only('BasicSearchManager.search()', () => { // expected defaults to false in this series! - const testCases: Array<{ input: string; expected?: number; first?: string; includes?: string; snapshot?: boolean; }> = [ - // valid dates - { input: `2022-01-11`, expected: 23, includes: "CVE-2022-21963", snapshot: true }, - // { input: `2025-01-11T02:31:22.611Z`, expected: 1, includes: "CVE-2024-42169", snapshot: true }, - { input: `2024-06-14`, expected: 4, includes: "CVE-2024-6006", snapshot: true }, - { input: `2025-01-11`, expected: 2, includes: "CVE-2024-42169", snapshot: true }, - { input: `1999`, expected: 0, snapshot: true }, - // { input: `2022`, expected: 0, snapshot: true }, - // { input: `2022-01`, expected: 12, includes: "CVE-2022-40621", snapshot: true }, // why search by month results in less than search by date in same month - // invalid dates + const responsecontains1 = [ + "CVE-2024-9420", + "CVE-2024-54014", + "CVE-2024-11921", + "CVE-2024-10451", + "CVE-2023-35159", + "CVE-2023-35158", + "CVE-2023-35157", + "CVE-2023-35156", + "CVE-2023-34254", + "CVE-2023-32728" + ]; + const testCases: Array<{ input: string; expected?: number; first?: string; contains?: string | string[]; snapshot?: boolean; }> = [ + // valid date variations, all the following are equivalent + { input: `2024-11-27`, expected: 11, contains: responsecontains1, snapshot: true }, + { input: `2024-11-27..2024-11-27`, expected: 11, contains: responsecontains1, snapshot: true }, + { input: `2024-11-27T00:00:00Z..2024-11-27T23:59:59Z`, expected: 11, contains: responsecontains1, snapshot: true }, + { input: `2024-11-27T00:00:00..2024-11-27T23:59:59`, expected: 11, contains: responsecontains1, snapshot: true }, + { input: `2024-11-27T00:00:00.000Z..2024-11-27T23:59:59.999Z`, expected: 11, contains: responsecontains1, snapshot: true }, + { input: `2024-11-27T00:00:00.000..2024-11-27T23:59:59.999`, expected: 11, contains: responsecontains1[0], snapshot: true }, + + // valid date ranges + { input: `2024-06-14`, expected: 4, contains: "CVE-2024-6006", snapshot: true }, + { input: `2025-01-11T02:31:22.611Z`, expected: 1, contains: "CVE-2024-42169", snapshot: true }, + { input: `2025-01-11T02:31:22`, expected: 1, contains: "CVE-2024-42169", snapshot: true }, + { input: `2024-11-27T09:00:00.000Z..2024-11-28T00:00:00.000Z`, expected: 10, contains: "CVE-2024-9420", snapshot: true }, + // { input: `1999`, expected: 0, snapshot: true }, // should work, but does not, but we don't want to support yet + // { input: `2025`, expected: 0, snapshot: true }, // should work, but does not, but we don't want to support yet + // { input: `2022-01`, expected: 12, contains: "CVE-2022-40621", snapshot: true }, // why search by month results in less than search by date in same month + + // invalid dates ]; - testCases.forEach(({ input, expected, first, includes, snapshot }) => { - it(`search('${input}') should return cves including ${includes}`, async () => { + testCases.forEach(({ input, expected, first, contains, snapshot }) => { + it(`search('${input}') should return cves including ${contains}`, async () => { const searchManager = new BasicSearchManager(searchProviderSpec); const resp = await searchManager.search(input, { track_total_hits: true, @@ -55,11 +76,13 @@ describe('Date Search (e2e tests)', () => { if (first) { expect(dat[0]["_id"]).toBe(first); } - if ( includes ) { - // let found = retlist.find( cve => { - // return cve["_id"] === includes - // }) - let found = retlist.some(item => item["_id"] === includes) + if (contains) { + if (!Array.isArray(contains)) { + contains = [contains]; + } + // make sure all items in contains appears in retlist + const idList = retlist.map(item => item["_id"]); + const found = contains.every(item => idList.includes(item)) expect( found ).toBeTruthy() } } diff --git a/src/search/test_cases/search_dates.test.unit.ts b/src/search/test_cases/search_dates.test.unit.ts index 7d2dbdc..e3ff56e 100644 --- a/src/search/test_cases/search_dates.test.unit.ts +++ b/src/search/test_cases/search_dates.test.unit.ts @@ -10,18 +10,19 @@ describe('Date Search (unit tests)', () => { // valid dates { input: `2025-09-18`, expected: true }, { input: `2024-02-29`, expected: true }, // leap year + { input: `1900-01-01`, expected: true }, // invalid dates (expected defaults to false in this series, so we can leave expect out) { input: null, expected: false }, { input: undefined }, { input: `` }, // empty string { input: ` ` }, // whitespace + { input: `1899-12-31`, expected: false }, // old date { input: `2025` }, // year only { input: `2025-01` }, // year and month only { input: `2025-09-01/2025-09-18` }, // date range { input: `2025-09-18T12:00:00:00.000Z` }, // datetime Z { input: `2025-09-18T12:00:00:00.000+05:00` }, // datetime offset { input: `2025-02-29` }, // not a leap year - { input: `1899-12-30` }, // out of range date { input: `T12:00:00:00.000Z` }, // ISO 8601 time // malformed dates