diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 93074bd..9837a92 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,7 +17,7 @@ updates: prefix-development: chore - package-ecosystem: docker - directory: "/test/shared/docker/eventsourcingdb" + directory: "/test/docker" schedule: interval: weekly open-pull-requests-limit: 10 diff --git a/README.md b/README.md index 884924c..a12f44a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # eventsourcingdb -eventsourcingdb is the official client for EventSourcingDB. +The official JavaScript client SDK for EventSourcingDB. diff --git a/biome.json b/biome.json index 8fb0b85..3a6e662 100644 --- a/biome.json +++ b/biome.json @@ -37,6 +37,9 @@ "enabled": true, "rules": { "all": true, + "complexity": { + "noExcessiveCognitiveComplexity": "off" + }, "correctness": { "noNodejsModules": "off" }, diff --git a/package-lock.json b/package-lock.json index 8798e70..a44db7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,10 @@ "name": "eventsourcingdb", "version": "0.9.1", "license": "MIT", - "dependencies": { - "axios": "1.8.4", - "http-status-codes": "2.3.0", - "stream-to-async-iterator": "1.0.0" - }, "devDependencies": { "@biomejs/biome": "1.9.4", - "@types/express": "5.0.1", "@types/node": "22.14.0", - "@types/shelljs": "0.8.15", - "express": "5.1.0", - "shelljs": "0.9.2", + "get-port": "7.1.0", "tsup": "8.4.0", "tsx": "4.19.3", "typescript": "5.8.3" @@ -190,9 +182,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], @@ -207,9 +199,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], @@ -224,9 +216,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], @@ -241,9 +233,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], @@ -258,9 +250,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], @@ -275,9 +267,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], @@ -292,9 +284,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], @@ -309,9 +301,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], @@ -326,9 +318,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], @@ -343,9 +335,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], @@ -360,9 +352,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], @@ -377,9 +369,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], @@ -394,9 +386,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], @@ -411,9 +403,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], @@ -428,9 +420,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], @@ -445,9 +437,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], @@ -462,9 +454,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], @@ -479,9 +471,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], @@ -496,9 +488,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], @@ -513,9 +505,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], @@ -530,9 +522,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], @@ -547,9 +539,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], @@ -564,9 +556,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], @@ -581,9 +573,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], @@ -598,9 +590,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], @@ -619,6 +611,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -631,90 +624,12 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -724,21 +639,12 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -748,52 +654,27 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } + "license": "MIT" }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@pkgjs/parseargs": { @@ -801,15 +682,16 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", - "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz", + "integrity": "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==", "cpu": [ "arm" ], @@ -821,9 +703,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", - "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz", + "integrity": "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==", "cpu": [ "arm64" ], @@ -835,9 +717,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz", + "integrity": "sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==", "cpu": [ "arm64" ], @@ -849,9 +731,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz", + "integrity": "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==", "cpu": [ "x64" ], @@ -863,9 +745,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", - "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz", + "integrity": "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==", "cpu": [ "arm64" ], @@ -877,9 +759,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", - "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz", + "integrity": "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==", "cpu": [ "x64" ], @@ -891,9 +773,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", - "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz", + "integrity": "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==", "cpu": [ "arm" ], @@ -905,9 +787,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", - "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz", + "integrity": "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==", "cpu": [ "arm" ], @@ -919,9 +801,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz", + "integrity": "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==", "cpu": [ "arm64" ], @@ -933,9 +815,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz", + "integrity": "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==", "cpu": [ "arm64" ], @@ -947,9 +829,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", - "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz", + "integrity": "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==", "cpu": [ "loong64" ], @@ -961,9 +843,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", - "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz", + "integrity": "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==", "cpu": [ "ppc64" ], @@ -975,9 +857,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", - "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz", + "integrity": "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz", + "integrity": "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==", "cpu": [ "riscv64" ], @@ -989,9 +885,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", - "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz", + "integrity": "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==", "cpu": [ "s390x" ], @@ -1003,9 +899,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz", + "integrity": "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==", "cpu": [ "x64" ], @@ -1017,9 +913,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz", + "integrity": "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==", "cpu": [ "x64" ], @@ -1031,9 +927,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz", + "integrity": "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==", "cpu": [ "arm64" ], @@ -1045,9 +941,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", - "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz", + "integrity": "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==", "cpu": [ "ia32" ], @@ -1059,9 +955,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz", + "integrity": "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==", "cpu": [ "x64" ], @@ -1072,78 +968,13 @@ "win32" ] }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, - "node_modules/@types/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", - "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", - "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true - }, "node_modules/@types/node": { "version": "22.14.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", @@ -1154,110 +985,27 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/qs": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", - "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/send/node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", - "dev": true, - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/@types/shelljs": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.15.tgz", - "integrity": "sha512-vzmnCHl6hViPu9GNLQJ+DZFd6BQI2DBTUeOvYHqkWQLMfKAAQYMb/xAmZkTogZI/vqXHCWkqDRymDI5p0QTi5Q==", - "dev": true, - "dependencies": { - "@types/glob": "~7.2.0", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" + "node": ">=12" }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1267,72 +1015,26 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } + "dev": true, + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -1349,54 +1051,30 @@ "esbuild": ">=0.18" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 14.16.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://paulmillr.com/funding/" } }, "node_modules/color-convert": { @@ -1404,6 +1082,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1415,94 +1094,29 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/consola": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", - "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1521,115 +1135,24 @@ } } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1640,288 +1163,96 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">= 18" + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 8" } }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/foreground-child/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, "engines": { "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=8" } }, - "node_modules/foreground-child": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", - "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, + "license": "MIT", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, "node_modules/foreground-child/node_modules/signal-exit": { @@ -1929,6 +1260,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -1936,36 +1268,20 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node": ">= 8" } }, "node_modules/fsevents": { @@ -1983,65 +1299,17 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/get-tsconfig": { @@ -2057,148 +1325,25 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-status-codes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", - "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/is-fullwidth-code-point": { @@ -2206,59 +1351,24 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -2274,15 +1384,17 @@ "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" }, @@ -2294,7 +1406,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/load-tsconfig": { "version": "0.2.5", @@ -2310,88 +1423,30 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } + "license": "ISC" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" + "node": ">=16 || 14 >=14.17" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -2399,6 +1454,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2415,143 +1471,36 @@ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -2563,39 +1512,19 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } + "license": "ISC" }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -2615,6 +1544,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "lilconfig": "^3.1.1" }, @@ -2642,134 +1572,28 @@ } } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/resolve-from": { @@ -2777,6 +1601,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2791,25 +1616,14 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", - "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz", + "integrity": "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -2819,341 +1633,58 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.9", - "@rollup/rollup-android-arm64": "4.34.9", - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-freebsd-arm64": "4.34.9", - "@rollup/rollup-freebsd-x64": "4.34.9", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", - "@rollup/rollup-linux-arm-musleabihf": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", - "@rollup/rollup-linux-riscv64-gnu": "4.34.9", - "@rollup/rollup-linux-s390x-gnu": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-ia32-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9", + "@rollup/rollup-android-arm-eabi": "4.39.0", + "@rollup/rollup-android-arm64": "4.39.0", + "@rollup/rollup-darwin-arm64": "4.39.0", + "@rollup/rollup-darwin-x64": "4.39.0", + "@rollup/rollup-freebsd-arm64": "4.39.0", + "@rollup/rollup-freebsd-x64": "4.39.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.39.0", + "@rollup/rollup-linux-arm-musleabihf": "4.39.0", + "@rollup/rollup-linux-arm64-gnu": "4.39.0", + "@rollup/rollup-linux-arm64-musl": "4.39.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.39.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.39.0", + "@rollup/rollup-linux-riscv64-gnu": "4.39.0", + "@rollup/rollup-linux-riscv64-musl": "4.39.0", + "@rollup/rollup-linux-s390x-gnu": "4.39.0", + "@rollup/rollup-linux-x64-gnu": "4.39.0", + "@rollup/rollup-linux-x64-musl": "4.39.0", + "@rollup/rollup-win32-arm64-msvc": "4.39.0", + "@rollup/rollup-win32-ia32-msvc": "4.39.0", + "@rollup/rollup-win32-x64-msvc": "4.39.0", "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/shelljs": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", - "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "execa": "^1.0.0", - "fast-glob": "^3.3.2", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-to-async-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-to-async-iterator/-/stream-to-async-iterator-1.0.0.tgz", - "integrity": "sha512-y7IQUStB2pOmq36KaOnLhaxIXjEYkKqzIxRW7grC3ByVKW7yDf88vXw9kS1wxdX5BrJvw/uh5N52NZ8COFy8tA==", - "engines": { - "node": ">=12" + "node": ">= 8" } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -3162,6 +1693,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3171,11 +1703,29 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3183,12 +1733,29 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3196,14 +1763,14 @@ "node": ">=8" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/sucrase": { @@ -3211,6 +1778,7 @@ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -3228,58 +1796,12 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0" } @@ -3289,6 +1811,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, + "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -3348,33 +1871,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", "dev": true, + "license": "MIT", "dependencies": { "punycode": "^2.1.0" } @@ -3384,6 +1886,7 @@ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, + "license": "MIT", "bin": { "tree-kill": "cli.js" } @@ -3392,7 +1895,8 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/tsup": { "version": "8.4.0", @@ -3446,36 +1950,6 @@ } } }, - "node_modules/tsup/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/tsup/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/tsx": { "version": "4.19.3", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", @@ -3496,44 +1970,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -3555,55 +1991,41 @@ "dev": true, "license": "MIT" }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, + "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi-cjs": { @@ -3612,6 +2034,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3624,12 +2047,66 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index 7bc21d4..7425aed 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,11 @@ { "name": "eventsourcingdb", "version": "0.9.1", - "description": "eventsourcingdb is the official client for EventSourcingDB.", - "contributors": [ - { - "name": "the native web GmbH", - "email": "hello@thenativeweb.io" - } - ], + "description": "The official JavaScript client SDK for EventSourcingDB.", + "author": { + "name": "the native web GmbH", + "email": "hello@thenativeweb.io" + }, "exports": { ".": { "import": "./dist/index.mjs", @@ -15,34 +13,35 @@ } }, "types": "./dist/index.d.ts", - "dependencies": { - "axios": "1.8.4", - "http-status-codes": "2.3.0", - "stream-to-async-iterator": "1.0.0" - }, "devDependencies": { "@biomejs/biome": "1.9.4", - "@types/express": "5.0.1", "@types/node": "22.14.0", - "@types/shelljs": "0.8.15", - "express": "5.1.0", - "shelljs": "0.9.2", + "get-port": "7.1.0", "tsup": "8.4.0", "tsx": "4.19.3", "typescript": "5.8.3" }, "scripts": { - "analyze": "npx biome check .", + "analyze": "npx biome check --error-on-warnings .", "build": "npx tsc --noEmit && npx tsup --clean --dts --format cjs,esm --minify --out-dir=./dist/ ./src/index.ts", "format": "npx biome format --write .", "qa": "npm run analyze && npm run test", "test": "npm run test:unit && npm run test:integration", - "test:unit": "node --test --import tsx \"./test/unit/**/*.ts\"", - "test:integration": "node --test --import tsx \"./test/integration/**/*.ts\"" + "test:unit": "node --test --import tsx \"./src/**/*.test.ts\"", + "test:integration": "node --test --import tsx \"./test/**/*.test.ts\"" }, "repository": { "type": "git", "url": "git://github.com/thenativeweb/eventsourcingdb-client-javascript.git" }, - "license": "MIT" + "license": "MIT", + "keywords": [ + "eventsourcingdb", + "esdb", + "event", + "events", + "event sourcing", + "event store", + "cqrs" + ] } diff --git a/src/Bound.ts b/src/Bound.ts new file mode 100644 index 0000000..6838f7a --- /dev/null +++ b/src/Bound.ts @@ -0,0 +1,8 @@ +type BoundType = 'inclusive' | 'exclusive'; + +interface Bound { + id: string; + type: BoundType; +} + +export type { BoundType, Bound }; diff --git a/src/Client.ts b/src/Client.ts index 391509d..1d41048 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,75 +1,429 @@ -import type { ClientConfiguration } from './ClientConfiguration.js'; -import type { ClientOptions } from './ClientOptions.js'; -import type { EventCandidate } from './event/EventCandidate.js'; -import type { EventContext } from './event/EventContext.js'; -import { getDefaultClientConfiguration } from './getDefaultClientConfiguration.js'; -import type { StoreItem } from './handlers/StoreItem.js'; -import type { ObserveEventsOptions } from './handlers/observeEvents/ObserveEventsOptions.js'; -import { observeEvents } from './handlers/observeEvents/observeEvents.js'; -import { ping } from './handlers/ping/ping.js'; -import type { EventType } from './handlers/readEventTypes/EventType.js'; -import { readEventTypes } from './handlers/readEventTypes/readEventTypes.js'; -import type { ReadEventsOptions } from './handlers/readEvents/ReadEventsOptions.js'; -import { readEvents } from './handlers/readEvents/readEvents.js'; -import type { ReadSubjectsOptions } from './handlers/readSubjects/ReadSubjectsOptions.js'; -import { readSubjects } from './handlers/readSubjects/readSubjects.js'; -import { registerEventSchema } from './handlers/registerEventSchema/registerEventSchema.js'; -import type { Precondition } from './handlers/writeEvents/Precondition.js'; -import { writeEvents } from './handlers/writeEvents/writeEvents.js'; -import { HttpClient } from './http/HttpClient.js'; +import type { Event } from './Event.js'; +import type { EventCandidate } from './EventCandidate.js'; +import type { EventType } from './EventType.js'; +import type { ObserveEventsOptions } from './ObserveEventsOptions.js'; +import type { Precondition } from './Precondition.js'; +import type { ReadEventsOptions } from './ReadEventsOptions.js'; +import { convertCloudEventToEvent } from './convertCloudEventToEvent.js'; +import { isCloudEvent } from './isCloudEvent.js'; +import { readNdJsonStream } from './ndjson/readNdJsonStream.js'; +import { isStreamCloudEvent } from './stream/isStreamCloudEvent.js'; +import { isStreamError } from './stream/isStreamError.js'; +import { isStreamEventType } from './stream/isStreamEventType.js'; +import { isStreamHeartbeat } from './stream/isStreamHeartbeat.js'; +import { isStreamSubject } from './stream/isStreamSubject.js'; +import { hasShapeOf } from './types/hasShapeOf.js'; class Client { - public readonly configuration: ClientConfiguration; - public readonly httpClient: HttpClient; - - public constructor(baseUrl: string, options: ClientOptions) { - this.configuration = { - ...getDefaultClientConfiguration(baseUrl), - ...options, - }; - this.httpClient = new HttpClient(this); + #url: URL; + #apiToken: string; + + #getUrl(path: string): string { + return new URL(path, this.#url).toString(); } - public observeEvents( - abortController: AbortController, - subject: string, - options: ObserveEventsOptions, - ): AsyncGenerator { - return observeEvents(this, abortController, subject, options); + public constructor(url: URL, apiToken: string) { + this.#url = url; + this.#apiToken = apiToken; } public async ping(): Promise { - await ping(this); + const url = this.#getUrl('/api/v1/ping'); + const response = await fetch(url, { + method: 'get', + }); + + if (response.status !== 200) { + throw new Error(`Failed to ping, got HTTP status code '${response.status}', expected '200'.`); + } + + const responseBody = await response.json(); + if (!hasShapeOf(responseBody, { type: 'string' })) { + throw new Error('Failed to parse response.'); + } + + const eventType = 'io.eventsourcingdb.api.ping-received'; + if (responseBody.type !== eventType) { + throw new Error('Failed to ping.'); + } + } + + public async verifyApiToken(): Promise { + const url = this.#getUrl('/api/v1/verify-api-token'); + const response = await fetch(url, { + method: 'post', + headers: { + authorization: `Bearer ${this.#apiToken}`, + }, + }); + + if (response.status !== 200) { + throw new Error( + `Failed to verify API token, got HTTP status code '${response.status}', expected '200'.`, + ); + } + + const responseBody = await response.json(); + if (!hasShapeOf(responseBody, { type: 'string' })) { + throw new Error('Failed to parse response.'); + } + + const eventType = 'io.eventsourcingdb.api.api-token-verified'; + if (responseBody.type !== eventType) { + throw new Error('Failed to verify API token.'); + } + } + + public async writeEvents( + events: EventCandidate[], + preconditions: Precondition[] = [], + ): Promise { + const url = this.#getUrl('/api/v1/write-events'); + const response = await fetch(url, { + method: 'post', + headers: { + authorization: `Bearer ${this.#apiToken}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + events, + preconditions, + }), + }); + + if (response.status !== 200) { + throw new Error( + `Failed to write events, got HTTP status code '${response.status}', expected '200'.`, + ); + } + + const responseBody = await response.json(); + + if (!Array.isArray(responseBody)) { + throw new Error('Failed to parse response.'); + } + + const writtenEvents = responseBody.map(item => { + if (!isCloudEvent(item)) { + throw new Error('Failed to parse response item.'); + } + + const event = convertCloudEventToEvent(item); + return event; + }); + + return writtenEvents; } public readEvents( - abortController: AbortController, subject: string, options: ReadEventsOptions, - ): AsyncGenerator { - return readEvents(this, abortController, subject, options); + signal?: AbortSignal, + ): AsyncGenerator { + const url = this.#getUrl('/api/v1/read-events'); + const apiToken = this.#apiToken; + + return (async function* () { + const internalAbortController = new AbortController(); + const combinedSignal = signal ?? internalAbortController.signal; + const shouldAbortInternally = !signal; + + let removeAbortListener: (() => void) | undefined; + + if (signal && !signal.aborted) { + const onAbort = () => internalAbortController.abort(); + signal.addEventListener('abort', onAbort, { once: true }); + removeAbortListener = () => signal.removeEventListener('abort', onAbort); + } + + try { + const response = await fetch(url, { + method: 'post', + headers: { + authorization: `Bearer ${apiToken}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + subject, + options, + }), + signal: combinedSignal, + }); + + if (response.status !== 200) { + throw new Error( + `Failed to read events, got HTTP status code '${response.status}', expected '200'.`, + ); + } + if (!response.body) { + throw new Error('Failed to read events.'); + } + + for await (const line of readNdJsonStream(response.body, combinedSignal)) { + if (isStreamHeartbeat(line)) { + continue; + } + if (isStreamError(line)) { + throw new Error(`${line.payload.error}.`); + } + if (isStreamCloudEvent(line)) { + const event = convertCloudEventToEvent(line.payload); + yield event; + continue; + } + + throw new Error('Failed to read events.'); + } + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + return; + } + throw error; + } finally { + if (removeAbortListener) { + removeAbortListener(); + } + if (shouldAbortInternally) { + internalAbortController.abort(); + } + } + })(); } - public readEventTypes(abortController: AbortController): AsyncGenerator { - return readEventTypes(this, abortController); + public observeEvents( + subject: string, + options: ObserveEventsOptions, + signal?: AbortSignal, + ): AsyncGenerator { + const url = this.#getUrl('/api/v1/observe-events'); + const apiToken = this.#apiToken; + + return (async function* () { + const internalAbortController = new AbortController(); + const combinedSignal = signal ?? internalAbortController.signal; + const shouldAbortInternally = !signal; + + let removeAbortListener: (() => void) | undefined; + + if (signal && !signal.aborted) { + const onAbort = () => internalAbortController.abort(); + signal.addEventListener('abort', onAbort, { once: true }); + removeAbortListener = () => signal.removeEventListener('abort', onAbort); + } + + try { + const response = await fetch(url, { + method: 'post', + headers: { + authorization: `Bearer ${apiToken}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + subject, + options, + }), + signal: combinedSignal, + }); + + if (response.status !== 200) { + throw new Error( + `Failed to observe events, got HTTP status code '${response.status}', expected '200'.`, + ); + } + if (!response.body) { + throw new Error('Failed to observe events.'); + } + + for await (const line of readNdJsonStream(response.body, combinedSignal)) { + if (isStreamHeartbeat(line)) { + continue; + } + if (isStreamError(line)) { + throw new Error(`${line.payload.error}.`); + } + if (isStreamCloudEvent(line)) { + const event = convertCloudEventToEvent(line.payload); + yield event; + continue; + } + + throw new Error('Failed to observe events.'); + } + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + return; + } + throw error; + } finally { + if (removeAbortListener) { + removeAbortListener(); + } + if (shouldAbortInternally) { + internalAbortController.abort(); + } + } + })(); + } + + public async registerEventSchema( + eventType: string, + schema: Record, + ): Promise { + const url = this.#getUrl('/api/v1/register-event-schema'); + const response = await fetch(url, { + method: 'post', + headers: { + authorization: `Bearer ${this.#apiToken}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + eventType, + schema, + }), + }); + + if (response.status !== 200) { + throw new Error( + `Failed to register event schema, got HTTP status code '${response.status}', expected '200'.`, + ); + } } public readSubjects( - abortController: AbortController, - options: ReadSubjectsOptions, + baseSubject: string, + signal?: AbortSignal, ): AsyncGenerator { - return readSubjects(this, abortController, options); - } + const url = this.#getUrl('/api/v1/read-subjects'); + const apiToken = this.#apiToken; + + return (async function* () { + const internalAbortController = new AbortController(); + const combinedSignal = signal ?? internalAbortController.signal; + const shouldAbortInternally = !signal; - public async registerEventSchema(eventType: string, schema: string | object): Promise { - return await registerEventSchema(this, eventType, schema); + let removeAbortListener: (() => void) | undefined; + + if (signal && !signal.aborted) { + const onAbort = () => internalAbortController.abort(); + signal.addEventListener('abort', onAbort, { once: true }); + removeAbortListener = () => signal.removeEventListener('abort', onAbort); + } + + try { + const response = await fetch(url, { + method: 'post', + headers: { + authorization: `Bearer ${apiToken}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + baseSubject, + }), + signal: combinedSignal, + }); + + if (response.status !== 200) { + throw new Error( + `Failed to read subjects, got HTTP status code '${response.status}', expected '200'.`, + ); + } + if (!response.body) { + throw new Error('Failed to read subjects.'); + } + + for await (const line of readNdJsonStream(response.body, combinedSignal)) { + if (isStreamHeartbeat(line)) { + continue; + } + if (isStreamError(line)) { + throw new Error(`${line.payload.error}.`); + } + if (isStreamSubject(line)) { + yield line.payload.subject; + continue; + } + + throw new Error('Failed to read subjects.'); + } + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + return; + } + throw error; + } finally { + if (removeAbortListener) { + removeAbortListener(); + } + if (shouldAbortInternally) { + internalAbortController.abort(); + } + } + })(); } - public async writeEvents( - eventCandidates: EventCandidate[], - preconditions: Precondition[] = [], - ): Promise { - return await writeEvents(this, eventCandidates, preconditions); + public readEventTypes(signal?: AbortSignal): AsyncGenerator { + const url = this.#getUrl('/api/v1/read-event-types'); + const apiToken = this.#apiToken; + + return (async function* () { + const internalAbortController = new AbortController(); + const combinedSignal = signal ?? internalAbortController.signal; + const shouldAbortInternally = !signal; + + let removeAbortListener: (() => void) | undefined; + + if (signal && !signal.aborted) { + const onAbort = () => internalAbortController.abort(); + signal.addEventListener('abort', onAbort, { once: true }); + removeAbortListener = () => signal.removeEventListener('abort', onAbort); + } + + try { + const response = await fetch(url, { + method: 'post', + headers: { + authorization: `Bearer ${apiToken}`, + }, + signal: combinedSignal, + }); + + if (response.status !== 200) { + throw new Error( + `Failed to read event types, got HTTP status code '${response.status}', expected '200'.`, + ); + } + if (!response.body) { + throw new Error('Failed to read event types.'); + } + + for await (const line of readNdJsonStream(response.body, combinedSignal)) { + if (isStreamHeartbeat(line)) { + continue; + } + if (isStreamError(line)) { + throw new Error(`${line.payload.error}.`); + } + if (isStreamEventType(line)) { + yield line.payload; + continue; + } + + throw new Error('Failed to read event types.'); + } + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + return; + } + throw error; + } finally { + if (removeAbortListener) { + removeAbortListener(); + } + if (shouldAbortInternally) { + internalAbortController.abort(); + } + } + })(); } } diff --git a/src/ClientConfiguration.ts b/src/ClientConfiguration.ts deleted file mode 100644 index a88ec5e..0000000 --- a/src/ClientConfiguration.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface ClientConfiguration { - baseUrl: string; - timeoutMilliseconds: number; - accessToken: string; - protocolVersion: string; -} - -export type { ClientConfiguration }; diff --git a/src/ClientOptions.ts b/src/ClientOptions.ts deleted file mode 100644 index 03fd54e..0000000 --- a/src/ClientOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -interface ClientOptions { - timeoutMilliseconds?: number; - accessToken: string; - protocolVersion?: string; -} - -export type { ClientOptions }; diff --git a/src/CloudEvent.ts b/src/CloudEvent.ts new file mode 100644 index 0000000..f10989c --- /dev/null +++ b/src/CloudEvent.ts @@ -0,0 +1,16 @@ +interface CloudEvent { + specversion: string; + id: string; + time: string; + source: string; + subject: string; + type: string; + datacontenttype: string; + data: Record; + hash: string; + predecessorhash: string; + traceparent?: string; + tracestate?: string; +} + +export type { CloudEvent }; diff --git a/src/Event.ts b/src/Event.ts new file mode 100644 index 0000000..9b1906d --- /dev/null +++ b/src/Event.ts @@ -0,0 +1,16 @@ +interface Event { + specversion: string; + id: string; + time: Date; + source: string; + subject: string; + type: string; + datacontenttype: string; + data: Record; + hash: string; + predecessorhash: string; + traceparent?: string; + tracestate?: string; +} + +export type { Event }; diff --git a/src/EventCandidate.ts b/src/EventCandidate.ts new file mode 100644 index 0000000..c20ea62 --- /dev/null +++ b/src/EventCandidate.ts @@ -0,0 +1,10 @@ +interface EventCandidate { + source: string; + subject: string; + type: string; + data: Record; + traceparent?: string; + tracestate?: string; +} + +export type { EventCandidate }; diff --git a/src/EventType.ts b/src/EventType.ts new file mode 100644 index 0000000..3da34b4 --- /dev/null +++ b/src/EventType.ts @@ -0,0 +1,7 @@ +interface EventType { + eventType: string; + isPhantom: boolean; + schema?: Record; +} + +export type { EventType }; diff --git a/src/ObserveEventsOptions.ts b/src/ObserveEventsOptions.ts new file mode 100644 index 0000000..b95da40 --- /dev/null +++ b/src/ObserveEventsOptions.ts @@ -0,0 +1,17 @@ +import type { Bound } from './Bound.js'; + +type ObserveIfEventIsMissing = 'read-everything' | 'wait-for-event'; + +interface ObserveFromLatestEvent { + subject: string; + type: string; + ifEventIsMissing: ObserveIfEventIsMissing; +} + +interface ObserveEventsOptions { + recursive: boolean; + lowerBound?: Bound; + fromLatestEvent?: ObserveFromLatestEvent; +} + +export type { ObserveIfEventIsMissing, ObserveFromLatestEvent, ObserveEventsOptions }; diff --git a/src/handlers/writeEvents/Precondition.ts b/src/Precondition.ts similarity index 51% rename from src/handlers/writeEvents/Precondition.ts rename to src/Precondition.ts index a7196a6..f7060dd 100644 --- a/src/handlers/writeEvents/Precondition.ts +++ b/src/Precondition.ts @@ -1,13 +1,3 @@ -type Precondition = - | { - type: 'isSubjectPristine'; - payload: IsSubjectPristinePrecondition; - } - | { - type: 'isSubjectOnEventId'; - payload: IsSubjectOnEventIdPrecondition; - }; - interface IsSubjectPristinePrecondition { subject: string; } @@ -17,19 +7,14 @@ interface IsSubjectOnEventIdPrecondition { eventId: string; } -const isSubjectPristine = (payload: IsSubjectPristinePrecondition): Precondition => { - return { - type: 'isSubjectPristine', - payload, - }; -}; - -const isSubjectOnEventId = (payload: IsSubjectOnEventIdPrecondition): Precondition => { - return { - type: 'isSubjectOnEventId', - payload, - }; -}; +type Precondition = + | { + type: 'isSubjectPristine'; + payload: IsSubjectPristinePrecondition; + } + | { + type: 'isSubjectOnEventId'; + payload: IsSubjectOnEventIdPrecondition; + }; export type { Precondition }; -export { isSubjectPristine, isSubjectOnEventId }; diff --git a/src/ReadEventsOptions.ts b/src/ReadEventsOptions.ts new file mode 100644 index 0000000..a792286 --- /dev/null +++ b/src/ReadEventsOptions.ts @@ -0,0 +1,21 @@ +import type { Bound } from './Bound.js'; + +type Order = 'chronological' | 'antichronological'; + +type ReadIfEventIsMissing = 'read-nothing' | 'read-everything'; + +interface ReadFromLatestEvent { + subject: string; + type: string; + ifEventIsMissing: ReadIfEventIsMissing; +} + +interface ReadEventsOptions { + recursive: boolean; + order?: Order; + lowerBound?: Bound; + upperBound?: Bound; + fromLatestEvent?: ReadFromLatestEvent; +} + +export type { Order, ReadIfEventIsMissing, ReadFromLatestEvent, ReadEventsOptions }; diff --git a/src/convertCloudEventToEvent.test.ts b/src/convertCloudEventToEvent.test.ts new file mode 100644 index 0000000..6010cf5 --- /dev/null +++ b/src/convertCloudEventToEvent.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { suite, test } from 'node:test'; +import type { CloudEvent } from './CloudEvent.js'; +import { convertCloudEventToEvent } from './convertCloudEventToEvent.js'; + +suite('convertCloudEventToEvent', () => { + test('converts a cloud event to an event.', () => { + const now = Date.now(); + + const cloudEvent: CloudEvent = { + specversion: '1.0', + id: '123', + time: new Date(now).toISOString(), + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + datacontenttype: 'application/json', + data: { key: 'value' }, + hash: '55a1f59420da66b2c4c87b565660054cff7c2aad5ebe5f56e04ae0f2a20f00a9', + predecessorhash: '4f67e993373952b6b6733a9b99de21842c42ed68ff881169ac914488b49dfeef', + }; + + const event = convertCloudEventToEvent(cloudEvent); + + assert.strictEqual(event.specversion, cloudEvent.specversion); + assert.strictEqual(event.id, cloudEvent.id); + assert.deepStrictEqual(event.time, new Date(now)); + assert.strictEqual(event.source, cloudEvent.source); + assert.strictEqual(event.subject, cloudEvent.subject); + assert.strictEqual(event.type, cloudEvent.type); + assert.strictEqual(event.datacontenttype, cloudEvent.datacontenttype); + assert.deepStrictEqual(event.data, cloudEvent.data); + assert.deepStrictEqual(event.hash, cloudEvent.hash); + assert.deepStrictEqual(event.predecessorhash, cloudEvent.predecessorhash); + }); +}); diff --git a/src/convertCloudEventToEvent.ts b/src/convertCloudEventToEvent.ts new file mode 100644 index 0000000..675a0f5 --- /dev/null +++ b/src/convertCloudEventToEvent.ts @@ -0,0 +1,11 @@ +import type { CloudEvent } from './CloudEvent.js'; +import type { Event } from './Event.js'; + +const convertCloudEventToEvent = (cloudEvent: CloudEvent): Event => { + return { + ...cloudEvent, + time: new Date(cloudEvent.time), + }; +}; + +export { convertCloudEventToEvent }; diff --git a/src/event/Event.ts b/src/event/Event.ts deleted file mode 100644 index 3582175..0000000 --- a/src/event/Event.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { UnknownObject } from '../util/UnknownObject.js'; -import { isObject } from '../util/isObject.js'; -import { EventContext } from './EventContext.js'; - -class Event extends EventContext { - public readonly data: Record; - - private constructor( - data: Record, - source: string, - subject: string, - type: string, - specVersion: string, - id: string, - time: Date, - dataContentType: string, - predecessorHash: string, - traceParent?: string, - traceState?: string, - ) { - super( - source, - subject, - type, - specVersion, - id, - time, - dataContentType, - predecessorHash, - traceParent, - traceState, - ); - this.data = data; - } - - public static parse(unknownObject: UnknownObject): Event { - const eventContext = EventContext.parse(unknownObject); - if (!isObject(unknownObject.data)) { - throw new Error(`Failed to parse data '${unknownObject.data}' to object.`); - } - - return new Event( - unknownObject.data, - eventContext.source, - eventContext.subject, - eventContext.type, - eventContext.specVersion, - eventContext.id, - eventContext.time, - eventContext.dataContentType, - eventContext.predecessorHash, - eventContext.traceParent, - eventContext.traceState, - ); - } - - // biome-ignore lint/style/useNamingConvention: We want to use this function name - public toJSON(): Record { - return { - ...super.toJSON(), - data: this.data, - }; - } -} - -export { Event }; diff --git a/src/event/EventCandidate.ts b/src/event/EventCandidate.ts deleted file mode 100644 index 1d53edd..0000000 --- a/src/event/EventCandidate.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { UnknownObject } from '../util/UnknownObject.js'; -import { ValidationError } from '../util/error/ValidationError.js'; -import { validateSubject } from './validateSubject.js'; -import { validateType } from './validateType.js'; - -class EventCandidate { - public readonly data: Record; - public readonly source: string; - public readonly subject: string; - public readonly type: string; - public readonly traceParent?: string; - public readonly traceState?: string; - - public constructor( - source: string, - subject: string, - type: string, - data: UnknownObject, - traceParent?: string, - traceState?: string, - ) { - this.data = data; - this.source = source; - this.subject = subject; - this.type = type; - this.traceParent = traceParent; - this.traceState = traceState; - } - - public validate(): void { - validateSubject(this.subject); - validateType(this.type); - - if (this.traceState !== undefined && this.traceParent === undefined) { - throw new ValidationError( - 'Failed to validate trace state: trace parent must be set if trace state is set.', - ); - } - } - // biome-ignore lint/style/useNamingConvention: We want to use this function name - public toJSON(): Record { - return { - data: this.data, - source: this.source, - subject: this.subject, - type: this.type, - traceparent: this.traceParent, - tracestate: this.traceState, - }; - } -} - -export { EventCandidate }; diff --git a/src/event/EventContext.ts b/src/event/EventContext.ts deleted file mode 100644 index ccc6b01..0000000 --- a/src/event/EventContext.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { UnknownObject } from '../util/UnknownObject.js'; -import { ValidationError } from '../util/error/ValidationError.js'; -import { validateSubject } from './validateSubject.js'; -import { validateType } from './validateType.js'; - -class EventContext { - public readonly source: string; - public readonly subject: string; - public readonly type: string; - public readonly specVersion: string; - public readonly id: string; - public readonly time: Date; - public readonly dataContentType: string; - public readonly predecessorHash: string; - public readonly traceParent?: string; - public readonly traceState?: string; - - protected constructor( - source: string, - subject: string, - type: string, - specVersion: string, - id: string, - time: Date, - dataContentType: string, - predecessorHash: string, - traceParent?: string, - traceState?: string, - ) { - this.source = source; - this.subject = subject; - this.type = type; - this.specVersion = specVersion; - this.id = id; - this.time = time; - this.dataContentType = dataContentType; - this.predecessorHash = predecessorHash; - this.traceParent = traceParent; - this.traceState = traceState; - } - - public static parse(unknownObject: UnknownObject): EventContext { - if (typeof unknownObject.source !== 'string') { - throw new ValidationError(`Failed to parse source '${unknownObject.source}' to string.`); - } - if (typeof unknownObject.subject !== 'string') { - throw new ValidationError(`Failed to parse subject '${unknownObject.subject}' to string.`); - } - validateSubject(unknownObject.subject); - if (typeof unknownObject.type !== 'string') { - throw new ValidationError(`Failed to parse type '${unknownObject.type}' to string.`); - } - validateType(unknownObject.type); - if (typeof unknownObject.specversion !== 'string') { - throw new ValidationError( - `Failed to parse specVersion '${unknownObject.specversion}' to string.`, - ); - } - if (typeof unknownObject.id !== 'string') { - throw new ValidationError(`Failed to parse id '${unknownObject.id}' to string.`); - } - if (typeof unknownObject.time !== 'string') { - throw new ValidationError(`Failed to parse time '${unknownObject.time}' to Date.`); - } - - const time = new Date(unknownObject.time); - - if (time.toString() === 'Invalid Date') { - throw new ValidationError(`Failed to parse time '${unknownObject.time}' to Date.`); - } - - if (typeof unknownObject.datacontenttype !== 'string') { - throw new ValidationError( - `Failed to parse dataContentType '${unknownObject.datacontenttype}' to string.`, - ); - } - if (typeof unknownObject.predecessorhash !== 'string') { - throw new ValidationError( - `Failed to parse predecessorHash '${unknownObject.predecessorhash}' to string.`, - ); - } - if (unknownObject.traceparent !== undefined && typeof unknownObject.traceparent !== 'string') { - throw new ValidationError( - `Failed to parse traceparent '${unknownObject.traceparent}' to string.`, - ); - } - if (unknownObject.tracestate !== undefined && typeof unknownObject.tracestate !== 'string') { - throw new ValidationError( - `Failed to parse tracestate '${unknownObject.tracestate}' to string.`, - ); - } - - return new EventContext( - unknownObject.source, - unknownObject.subject, - unknownObject.type, - unknownObject.specversion, - unknownObject.id, - time, - unknownObject.datacontenttype, - unknownObject.predecessorhash, - unknownObject.traceparent, - unknownObject.tracestate, - ); - } - - // biome-ignore lint/style/useNamingConvention: We want to use this function name - public toJSON(): Record { - return { - specversion: this.specVersion, - id: this.id, - time: this.time.toISOString(), - source: this.source, - subject: this.subject, - type: this.type, - datacontenttype: this.dataContentType, - predecessorhash: this.predecessorHash, - traceparent: this.traceParent, - tracestate: this.traceState, - }; - } - - public toString(): string { - return JSON.stringify(this); - } -} - -export { EventContext }; diff --git a/src/event/Source.ts b/src/event/Source.ts deleted file mode 100644 index 613dbcf..0000000 --- a/src/event/Source.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { UnknownObject } from '../util/UnknownObject.js'; -import { EventCandidate } from './EventCandidate.js'; - -class Source { - public readonly source: string; - - public constructor(source: string) { - this.source = source; - } - - public newEvent( - subject: string, - type: string, - data: UnknownObject, - traceParent?: string, - traceState?: string, - ): EventCandidate { - return new EventCandidate(this.source, subject, type, data, traceParent, traceState); - } -} - -export { Source }; diff --git a/src/event/validateSubject.ts b/src/event/validateSubject.ts deleted file mode 100644 index 71141ae..0000000 --- a/src/event/validateSubject.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ValidationError } from '../util/error/ValidationError.js'; - -const wordPattern = '[0-9A-Za-z_-]+'; -const subjectPattern = new RegExp(`^/(${wordPattern}/)*(${wordPattern}/?)?$`, 'u'); - -const validateSubject = (subject: string): void => { - const didMatch = subjectPattern.test(subject); - - if (!didMatch) { - throw new ValidationError( - `Failed to validate subject: '${subject}' must be an absolute, slash-separated path.`, - ); - } -}; - -export { validateSubject }; diff --git a/src/event/validateType.ts b/src/event/validateType.ts deleted file mode 100644 index 81fc90a..0000000 --- a/src/event/validateType.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ValidationError } from '../util/error/ValidationError.js'; - -const typePattern = /^[0-9A-Za-z_-]{2,}\.(?:[0-9A-Za-z_-]+\.)+[0-9A-Za-z_-]+$/u; - -const validateType = (type: string): void => { - const didMatch = typePattern.test(type); - - if (!didMatch) { - throw new ValidationError(`Failed to validate type: '${type}' must be a reverse domain name.`); - } -}; - -export { validateType }; diff --git a/src/getDefaultClientConfiguration.ts b/src/getDefaultClientConfiguration.ts deleted file mode 100644 index d19040e..0000000 --- a/src/getDefaultClientConfiguration.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ClientConfiguration } from './ClientConfiguration.js'; - -const getDefaultClientConfiguration = (baseUrl: string): ClientConfiguration => { - return { - baseUrl, - timeoutMilliseconds: 10_000, - accessToken: '', - protocolVersion: '1.0.0', - }; -}; - -export { getDefaultClientConfiguration }; diff --git a/src/handlers/Heartbeat.ts b/src/handlers/Heartbeat.ts deleted file mode 100644 index e12da18..0000000 --- a/src/handlers/Heartbeat.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface Heartbeat { - type: 'heartbeat'; -} - -export type { Heartbeat }; diff --git a/src/handlers/Item.ts b/src/handlers/Item.ts deleted file mode 100644 index e1222f2..0000000 --- a/src/handlers/Item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { UnknownObject } from '../util/UnknownObject.js'; - -interface Item { - type: 'item'; - payload: { - event: UnknownObject; - hash: string; - }; -} - -export type { Item }; diff --git a/src/handlers/StoreItem.ts b/src/handlers/StoreItem.ts deleted file mode 100644 index 50d36f7..0000000 --- a/src/handlers/StoreItem.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Event } from '../event/Event.js'; - -interface StoreItem { - event: Event; - hash: string; -} - -export type { StoreItem }; diff --git a/src/handlers/isHeartbeat.ts b/src/handlers/isHeartbeat.ts deleted file mode 100644 index e0e867a..0000000 --- a/src/handlers/isHeartbeat.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isObject } from '../util/isObject.js'; -import type { Heartbeat } from './Heartbeat.js'; - -const isHeartbeat = (message: unknown): message is Heartbeat => { - return isObject(message) && message.type === 'heartbeat'; -}; - -export { isHeartbeat }; diff --git a/src/handlers/isItem.ts b/src/handlers/isItem.ts deleted file mode 100644 index 524e550..0000000 --- a/src/handlers/isItem.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isObject } from '../util/isObject.js'; -import type { Item } from './Item.js'; - -const isItem = (message: unknown): message is Item => { - if (!isObject(message) || message.type !== 'item') { - return false; - } - - const { payload } = message; - - return isObject(payload) && isObject(payload.event) && typeof payload.hash === 'string'; -}; - -export { isItem }; diff --git a/src/handlers/isStreamError.ts b/src/handlers/isStreamError.ts deleted file mode 100644 index db039b9..0000000 --- a/src/handlers/isStreamError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isObject } from '../util/isObject.js'; -import type { StreamError } from './StreamError.js'; - -const isStreamError = (message: unknown): message is StreamError => { - if (!isObject(message) || message.type !== 'error') { - return false; - } - - const { payload } = message; - - return isObject(payload) && typeof payload.error === 'string'; -}; - -export { isStreamError }; diff --git a/src/handlers/observeEvents/ObserveEventsOptions.ts b/src/handlers/observeEvents/ObserveEventsOptions.ts deleted file mode 100644 index 962f686..0000000 --- a/src/handlers/observeEvents/ObserveEventsOptions.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { validateSubject } from '../../event/validateSubject.js'; -import { validateType } from '../../event/validateType.js'; -import { ValidationError } from '../../util/error/ValidationError.js'; -import { wrapError } from '../../util/error/wrapError.js'; -import { IsNonNegativeInteger } from '../../util/isNonNegativeInteger.js'; - -interface ObserveEventsOptions { - recursive: boolean; - lowerBoundId?: string; - fromLatestEvent?: ObserveFromLatestEvent; -} - -interface ObserveFromLatestEvent { - subject: string; - type: string; - ifEventIsMissing: 'read-everything' | 'wait-for-event'; -} - -const validateObserveEventsOptions = (options: ObserveEventsOptions): void => { - if (options.lowerBoundId !== undefined && !IsNonNegativeInteger(options.lowerBoundId)) { - throw new ValidationError( - 'ObserveEventsOptions are invalid: lowerBoundId must be 0 or greater.', - ); - } - - if (options.lowerBoundId !== undefined && options.fromLatestEvent !== undefined) { - throw new ValidationError( - 'ObserveEventsOptions are invalid: lowerBoundId and fromLatestEvent are mutually exclusive.', - ); - } - - const { fromLatestEvent } = options; - if (fromLatestEvent !== undefined) { - wrapError( - () => { - validateSubject(fromLatestEvent.subject); - validateType(fromLatestEvent.type); - }, - error => { - throw new ValidationError( - `ObserveEventsOptions are invalid: Failed to validate 'fromLatestEvent': ${error.message}`, - ); - }, - ); - } -}; - -export type { ObserveEventsOptions, ObserveFromLatestEvent }; -export { validateObserveEventsOptions }; diff --git a/src/handlers/observeEvents/observeEvents.ts b/src/handlers/observeEvents/observeEvents.ts deleted file mode 100644 index 4575f5f..0000000 --- a/src/handlers/observeEvents/observeEvents.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import type { Client } from '../../Client.js'; -import { Event } from '../../event/Event.js'; -import { validateSubject } from '../../event/validateSubject.js'; -import { CustomError } from '../../util/error/CustomError.js'; -import { InternalError } from '../../util/error/InternalError.js'; -import { InvalidParameterError } from '../../util/error/InvalidParameterError.js'; -import { ServerError } from '../../util/error/ServerError.js'; -import { ValidationError } from '../../util/error/ValidationError.js'; -import { wrapError } from '../../util/error/wrapError.js'; -import { readNdJsonStream } from '../../util/ndjson/readNdJsonStream.js'; -import type { StoreItem } from '../StoreItem.js'; -import { isHeartbeat } from '../isHeartbeat.js'; -import { isItem } from '../isItem.js'; -import { isStreamError } from '../isStreamError.js'; -import { validateObserveEventsOptions } from './ObserveEventsOptions.js'; -import type { ObserveEventsOptions } from './ObserveEventsOptions.js'; - -const observeEvents = async function* ( - client: Client, - abortController: AbortController, - subject: string, - options: ObserveEventsOptions, -): AsyncGenerator { - await wrapError( - () => validateSubject(subject), - ex => { - if (ex instanceof ValidationError) { - throw new InvalidParameterError('subject', ex.message); - } - }, - ); - await wrapError( - () => { - validateObserveEventsOptions(options); - }, - ex => { - if (ex instanceof ValidationError) { - throw new InvalidParameterError('options', ex.message); - } - }, - ); - - const requestBody = wrapError( - () => { - return JSON.stringify({ - subject, - options, - }); - }, - () => { - throw new InvalidParameterError( - 'options', - 'Parameter contains values that cannot be marshaled.', - ); - }, - ); - - const response = await wrapError( - async () => - client.httpClient.post({ - path: '/api/observe-events', - requestBody, - responseType: 'stream', - abortController, - }), - error => { - if (error instanceof CustomError) { - throw error; - } - - throw new InternalError(error); - }, - ); - - if (response.status !== StatusCodes.OK) { - throw new ServerError( - `Unexpected response status: ${response.status} ${getReasonPhrase(response.status)}.`, - ); - } - - const stream = response.data; - - for await (const message of readNdJsonStream(stream)) { - if (isHeartbeat(message)) { - continue; - } - if (isStreamError(message)) { - throw new ServerError(`${message.payload.error}.`); - } - if (isItem(message)) { - const event = Event.parse(message.payload.event); - - yield { - event, - hash: message.payload.hash, - }; - - continue; - } - - throw new ServerError( - `Failed to observe events, an unexpected stream item was received: '${JSON.stringify( - message, - )}'.`, - ); - } -}; - -export { observeEvents }; diff --git a/src/handlers/ping/ping.ts b/src/handlers/ping/ping.ts deleted file mode 100644 index a6b0d85..0000000 --- a/src/handlers/ping/ping.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Client } from '../../Client.js'; -import { CustomError } from '../../util/error/CustomError.js'; -import { InternalError } from '../../util/error/InternalError.js'; -import { ServerError } from '../../util/error/ServerError.js'; -import { wrapError } from '../../util/error/wrapError.js'; - -const ping = async (client: Client): Promise => { - const response = await wrapError( - async () => - client.httpClient.get({ - path: '/api/ping', - responseType: 'text', - withAuthorization: false, - }), - error => { - if (error instanceof CustomError) { - throw error; - } - - new InternalError(error); - }, - ); - - if (response.data !== 'OK') { - throw new ServerError('Received unexpected response.'); - } -}; - -export { ping }; diff --git a/src/handlers/readEventTypes/EventType.ts b/src/handlers/readEventTypes/EventType.ts deleted file mode 100644 index 2a5df21..0000000 --- a/src/handlers/readEventTypes/EventType.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isObject } from '../../util/isObject.js'; - -interface EventType { - eventType: string; - isPhantom: boolean; - schema?: string; -} - -const isEventType = (message: unknown): message is { payload: EventType } => { - if (!isObject(message) || message.type !== 'eventType') { - return false; - } - - return true; -}; - -export { isEventType }; -export type { EventType }; diff --git a/src/handlers/readEventTypes/readEventTypes.ts b/src/handlers/readEventTypes/readEventTypes.ts deleted file mode 100644 index 667d841..0000000 --- a/src/handlers/readEventTypes/readEventTypes.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; -import type { Client } from '../../Client.js'; -import { CustomError } from '../../util/error/CustomError.js'; -import { InternalError } from '../../util/error/InternalError.js'; -import { ServerError } from '../../util/error/ServerError.js'; -import { wrapError } from '../../util/error/wrapError.js'; -import { readNdJsonStream } from '../../util/ndjson/readNdJsonStream.js'; -import { isHeartbeat } from '../isHeartbeat.js'; -import { isStreamError } from '../isStreamError.js'; -import type { EventType } from './EventType.js'; -import { isEventType } from './EventType.js'; - -const readEventTypes = async function* ( - client: Client, - abortController: AbortController, -): AsyncGenerator { - const response = await wrapError( - async () => - client.httpClient.post({ - path: '/api/read-event-types', - requestBody: '', - responseType: 'stream', - abortController, - }), - error => { - if (error instanceof CustomError) { - throw error; - } - - throw new InternalError(error); - }, - ); - if (response.status !== StatusCodes.OK) { - throw new ServerError(`Unexpected response status: ${response.status} ${response.statusText}.`); - } - - const stream = response.data; - - for await (const message of readNdJsonStream(stream)) { - if (isHeartbeat(message)) { - continue; - } - if (isStreamError(message)) { - throw new ServerError(`${message.payload.error}.`); - } - if (isEventType(message)) { - yield message.payload; - continue; - } - - throw new ServerError( - `Failed to read events, an unexpected stream item was received: '${JSON.stringify( - message, - )}'.`, - ); - } -}; - -export { readEventTypes }; diff --git a/src/handlers/readEvents/ReadEventsOptions.ts b/src/handlers/readEvents/ReadEventsOptions.ts deleted file mode 100644 index 3498c4e..0000000 --- a/src/handlers/readEvents/ReadEventsOptions.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { validateSubject } from '../../event/validateSubject.js'; -import { validateType } from '../../event/validateType.js'; -import { ValidationError } from '../../util/error/ValidationError.js'; -import { wrapError } from '../../util/error/wrapError.js'; -import { IsNonNegativeInteger } from '../../util/isNonNegativeInteger.js'; - -interface ReadEventsOptions { - recursive: boolean; - order?: Order; - lowerBoundId?: string; - upperBoundId?: string; - fromLatestEvent?: ReadFromLatestEvent; -} - -type Order = 'chronological' | 'antichronological'; - -interface ReadFromLatestEvent { - subject: string; - type: string; - ifEventIsMissing: 'read-nothing' | 'read-everything'; -} - -const validateReadEventsOptions = (options: ReadEventsOptions): void => { - if (options.lowerBoundId !== undefined && !IsNonNegativeInteger(options.lowerBoundId)) { - throw new ValidationError('ReadEventsOptions are invalid: lowerBoundId must be 0 or greater.'); - } - if (options.upperBoundId !== undefined && !IsNonNegativeInteger(options.upperBoundId)) { - throw new ValidationError('ReadEventsOptions are invalid: upperBoundId must be 0 or greater.'); - } - - if (options.fromLatestEvent !== undefined) { - if (options.lowerBoundId !== undefined) { - throw new ValidationError( - 'ReadEventsOptions are invalid: lowerBoundId and fromLatestEvent are mutually exclusive.', - ); - } - - const { fromLatestEvent } = options; - wrapError( - () => { - validateSubject(fromLatestEvent.subject); - validateType(fromLatestEvent.type); - }, - error => { - throw new ValidationError( - `ReadEventsOptions are invalid: Failed to validate 'fromLatestEvent': ${error.message}`, - ); - }, - ); - } -}; - -export type { ReadEventsOptions, ReadFromLatestEvent }; -export { validateReadEventsOptions }; diff --git a/src/handlers/readEvents/readEvents.ts b/src/handlers/readEvents/readEvents.ts deleted file mode 100644 index 549db09..0000000 --- a/src/handlers/readEvents/readEvents.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; -import type { Client } from '../../Client.js'; -import { Event } from '../../event/Event.js'; -import { validateSubject } from '../../event/validateSubject.js'; -import { CustomError } from '../../util/error/CustomError.js'; -import { InternalError } from '../../util/error/InternalError.js'; -import { InvalidParameterError } from '../../util/error/InvalidParameterError.js'; -import { ServerError } from '../../util/error/ServerError.js'; -import { ValidationError } from '../../util/error/ValidationError.js'; -import { wrapError } from '../../util/error/wrapError.js'; -import { readNdJsonStream } from '../../util/ndjson/readNdJsonStream.js'; -import type { StoreItem } from '../StoreItem.js'; -import { isHeartbeat } from '../isHeartbeat.js'; -import { isItem } from '../isItem.js'; -import { isStreamError } from '../isStreamError.js'; -import type { ReadEventsOptions } from './ReadEventsOptions.js'; -import { validateReadEventsOptions } from './ReadEventsOptions.js'; - -const readEvents = async function* ( - client: Client, - abortController: AbortController, - subject: string, - options: ReadEventsOptions, -): AsyncGenerator { - wrapError( - () => validateSubject(subject), - ex => { - if (ex instanceof ValidationError) { - throw new InvalidParameterError('subject', ex.message); - } - }, - ); - wrapError( - () => { - validateReadEventsOptions(options); - }, - ex => { - if (ex instanceof ValidationError) { - throw new InvalidParameterError('options', ex.message); - } - }, - ); - - const requestBody = wrapError( - () => - JSON.stringify({ - subject, - options, - }), - () => { - throw new InvalidParameterError( - 'options', - 'Parameter contains values that cannot be marshaled.', - ); - }, - ); - - const response = await wrapError( - async () => - client.httpClient.post({ - path: '/api/read-events', - requestBody, - responseType: 'stream', - abortController, - }), - error => { - if (error instanceof CustomError) { - throw error; - } - - throw new InternalError(error); - }, - ); - if (response.status !== StatusCodes.OK) { - throw new ServerError(`Unexpected response status: ${response.status} ${response.statusText}.`); - } - - const stream = response.data; - - for await (const message of readNdJsonStream(stream)) { - if (isHeartbeat(message)) { - continue; - } - if (isStreamError(message)) { - throw new ServerError(`${message.payload.error}.`); - } - if (isItem(message)) { - const event = Event.parse(message.payload.event); - - yield { - event, - hash: message.payload.hash, - }; - - continue; - } - - throw new ServerError( - `Failed to read events, an unexpected stream item was received: '${JSON.stringify( - message, - )}'.`, - ); - } -}; - -export { readEvents }; diff --git a/src/handlers/readSubjects/ReadSubjectsOptions.ts b/src/handlers/readSubjects/ReadSubjectsOptions.ts deleted file mode 100644 index 544f559..0000000 --- a/src/handlers/readSubjects/ReadSubjectsOptions.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { validateSubject } from '../../event/validateSubject.js'; - -interface ReadSubjectsOptions { - baseSubject: string; -} - -const validateReadSubjectsOptions = (options: ReadSubjectsOptions): void => { - validateSubject(options.baseSubject); -}; - -export type { ReadSubjectsOptions }; -export { validateReadSubjectsOptions }; diff --git a/src/handlers/readSubjects/Subject.ts b/src/handlers/readSubjects/Subject.ts deleted file mode 100644 index f5fb66f..0000000 --- a/src/handlers/readSubjects/Subject.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface Subject { - type: 'subject'; - payload: { - subject: string; - }; -} - -export type { Subject }; diff --git a/src/handlers/readSubjects/isSubject.ts b/src/handlers/readSubjects/isSubject.ts deleted file mode 100644 index 11eee6e..0000000 --- a/src/handlers/readSubjects/isSubject.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isObject } from '../../util/isObject.js'; -import type { Subject } from './Subject.js'; - -const isSubject = (message: unknown): message is Subject => { - if (!isObject(message) || message.type !== 'subject') { - return false; - } - - const { payload } = message; - - return isObject(payload) && typeof payload.subject === 'string'; -}; - -export { isSubject }; diff --git a/src/handlers/readSubjects/readSubjects.ts b/src/handlers/readSubjects/readSubjects.ts deleted file mode 100644 index f01db3b..0000000 --- a/src/handlers/readSubjects/readSubjects.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import type { Client } from '../../Client.js'; -import { CustomError } from '../../util/error/CustomError.js'; -import { InternalError } from '../../util/error/InternalError.js'; -import { InvalidParameterError } from '../../util/error/InvalidParameterError.js'; -import { ServerError } from '../../util/error/ServerError.js'; -import { ValidationError } from '../../util/error/ValidationError.js'; -import { wrapError } from '../../util/error/wrapError.js'; -import { readNdJsonStream } from '../../util/ndjson/readNdJsonStream.js'; -import { isStreamError } from '../isStreamError.js'; -import type { ReadSubjectsOptions } from './ReadSubjectsOptions.js'; -import { validateReadSubjectsOptions } from './ReadSubjectsOptions.js'; -import { isSubject } from './isSubject.js'; - -const readSubjects = async function* ( - client: Client, - abortController: AbortController, - options: ReadSubjectsOptions, -): AsyncGenerator { - await wrapError( - () => { - validateReadSubjectsOptions(options); - }, - ex => { - if (ex instanceof ValidationError) { - throw new InvalidParameterError('options', ex.message); - } - }, - ); - - const requestBody = wrapError( - () => { - return JSON.stringify(options); - }, - _ex => { - throw new InvalidParameterError( - 'options', - 'Parameter contains values that cannot be marshaled.', - ); - }, - ); - - const response = await wrapError( - async () => - client.httpClient.post({ - path: '/api/read-subjects', - requestBody, - responseType: 'stream', - abortController, - }), - error => { - if (error instanceof CustomError) { - throw error; - } - - throw new InternalError(error); - }, - ); - - if (response.status !== StatusCodes.OK) { - throw new ServerError( - `Unexpected response status: ${response.status} ${getReasonPhrase(response.status)}.`, - ); - } - - const stream = response.data; - - for await (const message of readNdJsonStream(stream)) { - if (isStreamError(message)) { - throw new ServerError(message.payload.error); - } - - if (isSubject(message)) { - yield message.payload.subject; - continue; - } - - throw new ServerError( - `Failed to read subjects, an unexpected stream item was received: '${JSON.stringify( - message, - )}'.`, - ); - } -}; - -export { readSubjects }; diff --git a/src/handlers/registerEventSchema/registerEventSchema.ts b/src/handlers/registerEventSchema/registerEventSchema.ts deleted file mode 100644 index 5e577e6..0000000 --- a/src/handlers/registerEventSchema/registerEventSchema.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; -import type { Client } from '../../Client.js'; -import { validateType } from '../../event/validateType.js'; -import { CustomError } from '../../util/error/CustomError.js'; -import { InternalError } from '../../util/error/InternalError.js'; -import { InvalidParameterError } from '../../util/error/InvalidParameterError.js'; -import { ServerError } from '../../util/error/ServerError.js'; -import { ValidationError } from '../../util/error/ValidationError.js'; -import { wrapError } from '../../util/error/wrapError.js'; - -const registerEventSchema = async ( - client: Client, - eventType: string, - schema: object | string, -): Promise => { - wrapError( - () => validateType(eventType), - ex => { - if (ex instanceof ValidationError) { - throw new InvalidParameterError('eventType', ex.message); - } - }, - ); - - let schemaString = schema; - if (typeof schema === 'object') { - schemaString = JSON.stringify(schema); - } - - const requestBody = JSON.stringify({ - eventType, - schema: schemaString, - }); - - const response = await wrapError( - async () => - client.httpClient.post({ - path: '/api/register-event-schema', - requestBody, - responseType: 'text', - }), - error => { - if (error instanceof CustomError) { - throw error; - } - - throw new InternalError(error); - }, - ); - if (response.status !== StatusCodes.OK) { - throw new ServerError( - `Unexpected response status: ${response.status} ${response.statusText}: ${response.data}.`, - ); - } -}; - -export { registerEventSchema }; diff --git a/src/handlers/writeEvents/writeEvents.ts b/src/handlers/writeEvents/writeEvents.ts deleted file mode 100644 index 6f73668..0000000 --- a/src/handlers/writeEvents/writeEvents.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; -import type { Client } from '../../Client.js'; -import type { EventCandidate } from '../../event/EventCandidate.js'; -import { EventContext } from '../../event/EventContext.js'; -import { CustomError } from '../../util/error/CustomError.js'; -import { InternalError } from '../../util/error/InternalError.js'; -import { InvalidParameterError } from '../../util/error/InvalidParameterError.js'; -import { ServerError } from '../../util/error/ServerError.js'; -import { ValidationError } from '../../util/error/ValidationError.js'; -import { wrapError } from '../../util/error/wrapError.js'; -import type { Precondition } from './Precondition.js'; - -const writeEvents = async ( - client: Client, - eventCandidates: EventCandidate[], - preconditions: Precondition[], -): Promise => { - if (eventCandidates.length === 0) { - throw new InvalidParameterError( - 'eventCandidates', - 'eventCandidates must contain at least one EventCandidate.', - ); - } - for (const eventCandidate of eventCandidates) { - wrapError( - () => { - eventCandidate.validate(); - }, - ex => { - if (ex instanceof ValidationError) { - throw new InvalidParameterError('eventCandidates', ex.message); - } - }, - ); - } - - const requestBody = JSON.stringify({ - events: eventCandidates, - preconditions, - }); - - const response = await wrapError( - async () => - client.httpClient.post({ - path: '/api/write-events', - requestBody, - responseType: 'json', - }), - error => { - if (error instanceof CustomError) { - throw error; - } - - throw new InternalError(error); - }, - ); - if (response.status !== StatusCodes.OK) { - throw new ServerError(`Unexpected response status: ${response.status} ${response.statusText}.`); - } - - if (!Array.isArray(response.data)) { - throw new ServerError(`Failed to parse response '${response.data}' to array.`); - } - - const responseData = response.data; - return wrapError( - () => responseData.map((eventContext): EventContext => EventContext.parse(eventContext)), - error => { - if (error instanceof ValidationError) { - throw new ServerError(error.message); - } - - throw new InternalError(error); - }, - ); -}; - -export { writeEvents }; diff --git a/src/http/HttpClient.ts b/src/http/HttpClient.ts deleted file mode 100644 index 650429b..0000000 --- a/src/http/HttpClient.ts +++ /dev/null @@ -1,212 +0,0 @@ -import type { Readable } from 'node:stream'; -import type { AxiosResponse, CreateAxiosDefaults, ResponseType } from 'axios'; -import { AxiosError, CanceledError } from 'axios'; -import axios from 'axios'; -import { StatusCodes } from 'http-status-codes'; -import type { Client } from '../Client.js'; -import { CancelationError } from '../util/error/CancelationError.js'; -import { ClientError } from '../util/error/ClientError.js'; -import { CustomError } from '../util/error/CustomError.js'; -import { InternalError } from '../util/error/InternalError.js'; -import { ServerError } from '../util/error/ServerError.js'; - -// biome-ignore lint/style/useNamingConvention: We want to use this naming convention -type ResponseDataType = TResponseType extends 'arraybuffer' - ? ArrayBuffer - : TResponseType extends 'blob' - ? Blob - : TResponseType extends 'document' - ? unknown - : TResponseType extends 'json' - ? unknown - : TResponseType extends 'text' - ? string - : TResponseType extends 'stream' - ? Readable - : never; -// biome-ignore lint/style/useNamingConvention: We want to use this naming convention -type Response = AxiosResponse< - ResponseDataType, - unknown ->; - -class HttpClient { - private readonly databaseClient: Client; - - public constructor(dbClient: Client) { - this.databaseClient = dbClient; - } - - private getDefaultRequestConfig(withAuthorization = true): CreateAxiosDefaults { - let configuration: CreateAxiosDefaults = { - // biome-ignore lint/style/useNamingConvention: We want to use this naming convention - baseURL: this.databaseClient.configuration.baseUrl, - timeout: this.databaseClient.configuration.timeoutMilliseconds, - headers: { - 'X-EventSourcingDB-Protocol-Version': this.databaseClient.configuration.protocolVersion, - }, - validateStatus: () => true, - }; - - if (withAuthorization && this.databaseClient.configuration.accessToken !== undefined) { - configuration = HttpClient.setAuthorization( - configuration, - this.databaseClient.configuration.accessToken, - ); - } - - return configuration; - } - - private static setContentType( - configuration: CreateAxiosDefaults, - contentType: string, - ): CreateAxiosDefaults { - return { - ...configuration, - headers: { - ...configuration.headers, - 'Content-Type': contentType, - } as Record, - }; - } - - private static setAuthorization( - configuration: CreateAxiosDefaults, - accessToken: string, - ): CreateAxiosDefaults { - return { - ...configuration, - headers: { - ...configuration.headers, - authorization: `Bearer ${accessToken}`, - } as Record, - }; - } - - private static setResponseType( - configuration: CreateAxiosDefaults, - responseType: ResponseType, - ): CreateAxiosDefaults { - return { - ...configuration, - responseType, - }; - } - - public validateProtocolVersion(httpStatusCode: number, headers: Record): void { - if (httpStatusCode !== StatusCodes.UNPROCESSABLE_ENTITY) { - return; - } - - let serverProtocolVersion = headers['x-eventsourcingdb-protocol-version']; - - if (serverProtocolVersion === undefined) { - serverProtocolVersion = 'unknown version'; - } - - throw new ClientError( - `Protocol version mismatch, server '${serverProtocolVersion}', client '${this.databaseClient.configuration.protocolVersion}.'`, - ); - } - - // biome-ignore lint/style/useNamingConvention: We want to use this naming convention - public async post(options: { - path: string; - responseType: TResponseType; - requestBody: string; - abortController?: AbortController; - }): Promise> { - let configuration = this.getDefaultRequestConfig(); - configuration = HttpClient.setContentType(configuration, 'application/json'); - configuration = HttpClient.setResponseType(configuration, options.responseType); - const axiosInstance = axios.create(configuration); - - const abortController: AbortController = options.abortController ?? new AbortController(); - const signal = abortController.signal; - - try { - const response = await axiosInstance.post(options.path, options.requestBody, { signal }); - if (response.status >= 500 && response.status < 600) { - throw new ServerError(`Request failed with status code '${response.status}'.`); - } - - this.validateProtocolVersion(response.status, response.headers); - - if (response.status >= 400 && response.status < 500) { - throw new ClientError(`Request failed with status code '${response.status}'.`); - } - - return response; - } catch (ex) { - if (ex instanceof CustomError) { - throw ex; - } - - if (ex instanceof CanceledError) { - throw new CancelationError(); - } - - if (ex instanceof AxiosError) { - if (ex.request !== undefined) { - throw new ServerError('No response received.'); - } - throw new InternalError('Failed to setup request.'); - } - - throw new InternalError(ex); - } - } - - // biome-ignore lint/style/useNamingConvention: We want to use this naming convention - public async get(options: { - path: string; - responseType: TResponseType; - abortController?: AbortController; - withAuthorization?: boolean; - }): Promise> { - let configuration = this.getDefaultRequestConfig(options.withAuthorization); - configuration = HttpClient.setResponseType(configuration, options.responseType); - const axiosInstance = axios.create(configuration); - - const abortController: AbortController = options.abortController ?? new AbortController(); - const signal = abortController.signal; - - try { - const response = await axiosInstance.get(options.path, { signal }); - - if (response.status >= 500 && response.status < 600) { - throw new ServerError(`Request failed with status code '${response.status}'.`); - } - - this.validateProtocolVersion(response.status, response.headers); - - if (response.status >= 400 && response.status < 500) { - throw new ClientError(`Request failed with status code '${response.status}'.`); - } - - this.validateProtocolVersion(response.status, response.headers); - - return response; - } catch (ex) { - if (ex instanceof CustomError) { - throw ex; - } - - if (ex instanceof CanceledError) { - throw new CancelationError(); - } - - if (ex instanceof AxiosError) { - if (ex.request !== undefined) { - throw new ServerError('No response received.'); - } - throw new InternalError('Failed to setup request.'); - } - - throw new InternalError(ex); - } - } -} - -export { HttpClient }; diff --git a/src/index.ts b/src/index.ts index 3e64b67..c364caf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,11 @@ -// biome-ignore lint/performance/noBarrelFile: This is the main entry point for the library. -export { CancelationError } from './util/error/CancelationError.js'; -export { Client } from './Client.js'; -export type { ClientConfiguration } from './ClientConfiguration.js'; -export { Event } from './event/Event.js'; -export { EventCandidate } from './event/EventCandidate.js'; -export type { ReadSubjectsOptions } from './handlers/readSubjects/ReadSubjectsOptions.js'; -export { Source } from './event/Source.js'; -export type { StoreItem } from './handlers/StoreItem.js'; -export type { - ObserveEventsOptions, - ObserveFromLatestEvent, -} from './handlers/observeEvents/ObserveEventsOptions.js'; -export type { Precondition } from './handlers/writeEvents/Precondition.js'; -export { - isSubjectPristine, - isSubjectOnEventId, -} from './handlers/writeEvents/Precondition.js'; -export type { - ReadEventsOptions, - ReadFromLatestEvent, -} from './handlers/readEvents/ReadEventsOptions.js'; +import { Client } from './Client.js'; +import type { Event } from './Event.js'; +import type { EventCandidate } from './EventCandidate.js'; +import type { EventType } from './EventType.js'; +import type { ObserveEventsOptions } from './ObserveEventsOptions.js'; +import type { ReadEventsOptions } from './ReadEventsOptions.js'; +import { isSubjectOnEventId } from './isSubjectOnEventId.js'; +import { isSubjectPristine } from './isSubjectPristine.js'; + +export { Client, isSubjectPristine, isSubjectOnEventId }; +export type { Event, EventCandidate, EventType, ReadEventsOptions, ObserveEventsOptions }; diff --git a/src/isCloudEvent.ts b/src/isCloudEvent.ts new file mode 100644 index 0000000..4a860fc --- /dev/null +++ b/src/isCloudEvent.ts @@ -0,0 +1,21 @@ +import type { CloudEvent } from './CloudEvent.js'; +import { hasShapeOf } from './types/hasShapeOf.js'; + +const blueprint: CloudEvent = { + specversion: 'string', + id: 'string', + time: 'string', + source: 'string', + subject: 'string', + type: 'string', + datacontenttype: 'string', + data: {}, + hash: 'string', + predecessorhash: 'string', +}; + +const isCloudEvent = (value: unknown): value is CloudEvent => { + return hasShapeOf(value, blueprint); +}; + +export { isCloudEvent }; diff --git a/src/isSubjectOnEventId.ts b/src/isSubjectOnEventId.ts new file mode 100644 index 0000000..d453c39 --- /dev/null +++ b/src/isSubjectOnEventId.ts @@ -0,0 +1,10 @@ +import type { Precondition } from './Precondition.js'; + +const isSubjectOnEventId = (subject: string, eventId: string): Precondition => { + return { + type: 'isSubjectOnEventId', + payload: { subject, eventId }, + }; +}; + +export { isSubjectOnEventId }; diff --git a/src/isSubjectPristine.ts b/src/isSubjectPristine.ts new file mode 100644 index 0000000..6424241 --- /dev/null +++ b/src/isSubjectPristine.ts @@ -0,0 +1,10 @@ +import type { Precondition } from './Precondition.js'; + +const isSubjectPristine = (subject: string): Precondition => { + return { + type: 'isSubjectPristine', + payload: { subject }, + }; +}; + +export { isSubjectPristine }; diff --git a/src/ndjson/readNdJsonStream.test.ts b/src/ndjson/readNdJsonStream.test.ts new file mode 100644 index 0000000..263057f --- /dev/null +++ b/src/ndjson/readNdJsonStream.test.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import { Readable } from 'node:stream'; +import { suite, test } from 'node:test'; +import { readNdJsonStream } from './readNdJsonStream.js'; + +function convertStreamToWebStream(stream: Readable): ReadableStream { + return new ReadableStream({ + start(controller) { + stream.on('data', chunk => { + controller.enqueue(new Uint8Array(chunk)); + }); + stream.on('end', () => { + controller.close(); + }); + stream.on('error', err => { + controller.error(err); + }); + }, + }); +} + +suite('readNdJsonStream', (): void => { + test('returns an async generator that yields parsed json objects.', async (): Promise => { + const stream = convertStreamToWebStream( + Readable.from(Buffer.from('{"foo":"bar"}\n{"bar":"baz"}\n{"incomplete', 'utf-8')), + ); + + const controller = new AbortController(); + + const values: Record[] = []; + for await (const value of readNdJsonStream(stream, controller.signal)) { + values.push(value); + } + + assert.deepEqual(values, [{ foo: 'bar' }, { bar: 'baz' }]); + }); +}); diff --git a/src/ndjson/readNdJsonStream.ts b/src/ndjson/readNdJsonStream.ts new file mode 100644 index 0000000..f8bd455 --- /dev/null +++ b/src/ndjson/readNdJsonStream.ts @@ -0,0 +1,60 @@ +const readNdJsonStream = async function* ( + stream: ReadableStream, + signal: AbortSignal, +): AsyncGenerator, void, void> { + const reader = stream.getReader(); + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + + const onAbort = () => { + reader.cancel().catch(() => { + // Intentionally left blank. + }); + }; + + if (signal.aborted) { + await reader.cancel().catch(() => { + // Intentionally left blank. + }); + return; + } + + signal.addEventListener('abort', onAbort); + + try { + while (true) { + if (signal.aborted) { + break; + } + + const { done, value } = await reader.read(); + if (done) { + break; + } + + buffer += decoder.decode(value, { stream: true }); + + let index: number; + while (true) { + index = buffer.indexOf('\n'); + if (index === -1) { + break; + } + + const line = buffer.slice(0, index).trim(); + buffer = buffer.slice(index + 1); + + if (line) { + yield JSON.parse(line); + } + } + } + } finally { + signal.removeEventListener('abort', onAbort); + await reader.cancel().catch(() => { + // Intentionally left blank. + }); + } +}; + +export { readNdJsonStream }; diff --git a/src/stream/StreamCloudEvent.ts b/src/stream/StreamCloudEvent.ts new file mode 100644 index 0000000..2b8c752 --- /dev/null +++ b/src/stream/StreamCloudEvent.ts @@ -0,0 +1,19 @@ +interface StreamCloudEvent { + type: 'event'; + payload: { + specversion: string; + id: string; + time: string; + source: string; + subject: string; + type: string; + datacontenttype: string; + data: Record; + hash: string; + predecessorhash: string; + traceparent?: string; + tracestate?: string; + }; +} + +export type { StreamCloudEvent }; diff --git a/src/handlers/StreamError.ts b/src/stream/StreamError.ts similarity index 100% rename from src/handlers/StreamError.ts rename to src/stream/StreamError.ts diff --git a/src/stream/StreamEventType.ts b/src/stream/StreamEventType.ts new file mode 100644 index 0000000..557956f --- /dev/null +++ b/src/stream/StreamEventType.ts @@ -0,0 +1,10 @@ +interface StreamEventType { + type: 'eventType'; + payload: { + eventType: string; + isPhantom: boolean; + schema?: Record; + }; +} + +export type { StreamEventType }; diff --git a/src/stream/StreamHeartbeat.ts b/src/stream/StreamHeartbeat.ts new file mode 100644 index 0000000..f61653a --- /dev/null +++ b/src/stream/StreamHeartbeat.ts @@ -0,0 +1,5 @@ +interface StreamHeartbeat { + type: 'heartbeat'; +} + +export type { StreamHeartbeat }; diff --git a/src/stream/StreamSubject.ts b/src/stream/StreamSubject.ts new file mode 100644 index 0000000..fa50937 --- /dev/null +++ b/src/stream/StreamSubject.ts @@ -0,0 +1,8 @@ +interface StreamSubject { + type: 'subject'; + payload: { + subject: string; + }; +} + +export type { StreamSubject }; diff --git a/src/stream/isStreamCloudEvent.ts b/src/stream/isStreamCloudEvent.ts new file mode 100644 index 0000000..7a62f9a --- /dev/null +++ b/src/stream/isStreamCloudEvent.ts @@ -0,0 +1,31 @@ +import { hasShapeOf } from '../types/hasShapeOf.js'; +import type { StreamCloudEvent } from './StreamCloudEvent.js'; + +const blueprint: StreamCloudEvent = { + type: 'event', + payload: { + specversion: 'string', + id: 'string', + time: 'string', + source: 'string', + subject: 'string', + type: 'string', + datacontenttype: 'string', + data: {}, + hash: 'string', + predecessorhash: 'string', + }, +}; + +const isStreamCloudEvent = (line: unknown): line is StreamCloudEvent => { + if (!hasShapeOf(line, blueprint)) { + return false; + } + if (line.type !== 'event') { + return false; + } + + return true; +}; + +export { isStreamCloudEvent }; diff --git a/src/stream/isStreamError.ts b/src/stream/isStreamError.ts new file mode 100644 index 0000000..7ee6fa7 --- /dev/null +++ b/src/stream/isStreamError.ts @@ -0,0 +1,22 @@ +import { hasShapeOf } from '../types/hasShapeOf.js'; +import type { StreamError } from './StreamError.js'; + +const blueprint: StreamError = { + type: 'error', + payload: { + error: 'string', + }, +}; + +const isStreamError = (line: unknown): line is StreamError => { + if (!hasShapeOf(line, blueprint)) { + return false; + } + if (line.type !== 'error') { + return false; + } + + return true; +}; + +export { isStreamError }; diff --git a/src/stream/isStreamEventType.ts b/src/stream/isStreamEventType.ts new file mode 100644 index 0000000..a40ac7b --- /dev/null +++ b/src/stream/isStreamEventType.ts @@ -0,0 +1,23 @@ +import { hasShapeOf } from '../types/hasShapeOf.js'; +import type { StreamEventType } from './StreamEventType.js'; + +const blueprint: StreamEventType = { + type: 'eventType', + payload: { + eventType: 'string', + isPhantom: true, + }, +}; + +const isStreamEventType = (line: unknown): line is StreamEventType => { + if (!hasShapeOf(line, blueprint)) { + return false; + } + if (line.type !== 'eventType') { + return false; + } + + return true; +}; + +export { isStreamEventType }; diff --git a/src/stream/isStreamHeartbeat.ts b/src/stream/isStreamHeartbeat.ts new file mode 100644 index 0000000..75b8f27 --- /dev/null +++ b/src/stream/isStreamHeartbeat.ts @@ -0,0 +1,19 @@ +import { hasShapeOf } from '../types/hasShapeOf.js'; +import type { StreamHeartbeat } from './StreamHeartbeat.js'; + +const blueprint: StreamHeartbeat = { + type: 'heartbeat', +}; + +const isStreamHeartbeat = (line: unknown): line is StreamHeartbeat => { + if (!hasShapeOf(line, blueprint)) { + return false; + } + if (line.type !== 'heartbeat') { + return false; + } + + return true; +}; + +export { isStreamHeartbeat }; diff --git a/src/stream/isStreamSubject.ts b/src/stream/isStreamSubject.ts new file mode 100644 index 0000000..28c2513 --- /dev/null +++ b/src/stream/isStreamSubject.ts @@ -0,0 +1,22 @@ +import { hasShapeOf } from '../types/hasShapeOf.js'; +import type { StreamSubject } from './StreamSubject.js'; + +const blueprint: StreamSubject = { + type: 'subject', + payload: { + subject: 'string', + }, +}; + +const isStreamSubject = (line: unknown): line is StreamSubject => { + if (!hasShapeOf(line, blueprint)) { + return false; + } + if (line.type !== 'subject') { + return false; + } + + return true; +}; + +export { isStreamSubject }; diff --git a/src/types/hasShapeOf.test.ts b/src/types/hasShapeOf.test.ts new file mode 100644 index 0000000..3274163 --- /dev/null +++ b/src/types/hasShapeOf.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import { suite, test } from 'node:test'; +import { hasShapeOf } from './hasShapeOf.js'; + +suite('hasShapeOf', (): void => { + test('returns true for the same primitive type.', (): void => { + const object = 'foo'; + const shape = 'bar'; + assert.ok(hasShapeOf(object, shape)); + }); + + test('returns false for different primitive types.', (): void => { + const object = 'foo'; + const shape = 42; + assert.ok(!hasShapeOf(object, shape)); + }); + + test('returns true if the object has the shape.', (): void => { + const object = { type: 'foo' }; + const shape = { type: 'bar' }; + assert.ok(hasShapeOf(object, shape)); + }); + + test('returns false if the object does not have the shape.', (): void => { + const object = { type: 'foo' }; + const shape = { type: 42 }; + assert.ok(!hasShapeOf(object, shape)); + }); + + test('returns false if the object is null.', (): void => { + const object: null = null; + const shape = { type: 'foo' }; + assert.ok(!hasShapeOf(object, shape)); + }); +}); diff --git a/src/types/hasShapeOf.ts b/src/types/hasShapeOf.ts new file mode 100644 index 0000000..d43ebbc --- /dev/null +++ b/src/types/hasShapeOf.ts @@ -0,0 +1,33 @@ +const hasShapeOf = (value: unknown, blueprint: T): value is T => { + if (typeof blueprint !== typeof value) { + return false; + } + + if (blueprint === null || value === null) { + return blueprint === null && value === null; + } + + if (typeof blueprint === 'object') { + if (Array.isArray(blueprint) || Array.isArray(value)) { + return false; + } + + const blueprintObject = blueprint as Record; + const valueObject = value as Record; + + for (const key of Object.keys(blueprintObject)) { + if (!(key in valueObject)) { + return false; + } + if (!hasShapeOf(valueObject[key], blueprintObject[key])) { + return false; + } + } + + return true; + } + + return true; +}; + +export { hasShapeOf }; diff --git a/src/util/UnknownObject.ts b/src/util/UnknownObject.ts deleted file mode 100644 index 2a8f7c7..0000000 --- a/src/util/UnknownObject.ts +++ /dev/null @@ -1,3 +0,0 @@ -type UnknownObject = Partial>; - -export type { UnknownObject }; diff --git a/src/util/error/CancelationError.ts b/src/util/error/CancelationError.ts deleted file mode 100644 index 052edf2..0000000 --- a/src/util/error/CancelationError.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CustomError } from './CustomError.js'; - -class CancelationError extends CustomError {} - -export { CancelationError }; diff --git a/src/util/error/ClientError.ts b/src/util/error/ClientError.ts deleted file mode 100644 index c219dad..0000000 --- a/src/util/error/ClientError.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CustomError } from './CustomError.js'; - -class ClientError extends CustomError { - constructor(cause: string) { - super(`Client error occurred: ${cause}`); - } -} - -export { ClientError }; diff --git a/src/util/error/CustomError.ts b/src/util/error/CustomError.ts deleted file mode 100644 index 56e4b21..0000000 --- a/src/util/error/CustomError.ts +++ /dev/null @@ -1,3 +0,0 @@ -class CustomError extends Error {} - -export { CustomError }; diff --git a/src/util/error/InternalError.ts b/src/util/error/InternalError.ts deleted file mode 100644 index d77db4a..0000000 --- a/src/util/error/InternalError.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CustomError } from './CustomError.js'; - -class InternalError extends CustomError { - constructor(cause: unknown) { - super(`Internal error occurred: ${cause}`); - } -} - -export { InternalError }; diff --git a/src/util/error/InvalidParameterError.ts b/src/util/error/InvalidParameterError.ts deleted file mode 100644 index ede4def..0000000 --- a/src/util/error/InvalidParameterError.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CustomError } from './CustomError.js'; - -class InvalidParameterError extends CustomError { - constructor(parameterName: string, reason: string) { - super(`Parameter '${parameterName}' is invalid: ${reason}`); - } -} - -export { InvalidParameterError }; diff --git a/src/util/error/ServerError.ts b/src/util/error/ServerError.ts deleted file mode 100644 index c85f83f..0000000 --- a/src/util/error/ServerError.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CustomError } from './CustomError.js'; - -class ServerError extends CustomError { - constructor(cause: string) { - super(`Server error occurred: ${cause}`); - } -} - -export { ServerError }; diff --git a/src/util/error/ValidationError.ts b/src/util/error/ValidationError.ts deleted file mode 100644 index 5a0adbc..0000000 --- a/src/util/error/ValidationError.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CustomError } from './CustomError.js'; - -class ValidationError extends CustomError {} - -export { ValidationError }; diff --git a/src/util/error/wrapError.ts b/src/util/error/wrapError.ts deleted file mode 100644 index 46531c4..0000000 --- a/src/util/error/wrapError.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { InternalError } from './InternalError.js'; - -const isPromise = (value: unknown): value is Promise => { - return typeof value === 'object' && typeof (value as Record).then === 'function'; -}; - -// biome-ignore lint/style/useNamingConvention: We want to use this return type -function wrapError(fn: () => TReturn, onError: (error: Error) => void): TReturn; -// biome-ignore lint/style/useNamingConvention: We want to use this return type -function wrapError( - fn: () => Promise, - onError: (error: Error) => Promise, -): Promise; -// biome-ignore lint/style/useNamingConvention: We want to use this return type -function wrapError( - fn: () => TReturn | Promise, - onError: (error: Error) => void | Promise, -): TReturn | Promise { - try { - const invocationResult = fn(); - - if (isPromise(invocationResult)) { - return invocationResult.catch((ex: unknown) => { - let err: Error; - - if (ex instanceof Error) { - err = ex; - } else { - err = new InternalError(ex); - } - - try { - const onErrorInvocationResult = onError(err); - - if (isPromise(onErrorInvocationResult)) { - return onErrorInvocationResult - .then(() => { - return Promise.reject(new InternalError('onError did not throw.')); - }) - .catch((newEx: unknown): Promise => { - let newErr: Error; - - if (newEx instanceof Error) { - newErr = newEx; - } else { - newErr = new InternalError(newEx); - } - - return Promise.reject(newErr); - }); - } - - return Promise.reject(err); - } catch (newEx: unknown) { - let newErr: Error; - - if (newEx instanceof Error) { - newErr = newEx; - } else { - newErr = new InternalError(newEx); - } - - return Promise.reject(newErr); - } - }); - } - - return invocationResult; - } catch (ex: unknown) { - if (ex instanceof Error) { - onError(ex); - } - - throw new InternalError(ex); - } -} - -export { wrapError }; diff --git a/src/util/isNonNegativeInteger.ts b/src/util/isNonNegativeInteger.ts deleted file mode 100644 index 3138896..0000000 --- a/src/util/isNonNegativeInteger.ts +++ /dev/null @@ -1,7 +0,0 @@ -const nonNegativeIntegerMatcher = /^\d+$/u; - -const IsNonNegativeInteger = (value: string): boolean => { - return nonNegativeIntegerMatcher.test(value); -}; - -export { IsNonNegativeInteger }; diff --git a/src/util/isObject.ts b/src/util/isObject.ts deleted file mode 100644 index eeaf89d..0000000 --- a/src/util/isObject.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { UnknownObject } from './UnknownObject.js'; - -const isObject = (value: unknown): value is UnknownObject => { - return typeof value === 'object' && !Array.isArray(value) && value !== null; -}; - -export { isObject }; diff --git a/src/util/ndjson/LinesDecoder.ts b/src/util/ndjson/LinesDecoder.ts deleted file mode 100644 index 8680d66..0000000 --- a/src/util/ndjson/LinesDecoder.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { StringDecoder } from 'node:string_decoder'; - -class LinesDecoder { - private textBuffer: string; - private readonly decoder: StringDecoder; - - // biome-ignore lint/correctness/noUndeclaredVariables: This is a global variable defined by Node.js. - public constructor(encoding?: BufferEncoding) { - this.textBuffer = ''; - this.decoder = new StringDecoder(encoding); - } - - public write(chunk: Buffer): string[] { - this.textBuffer += this.decoder.write(chunk); - - const lines: string[] = []; - - let lineStart = 0; - for (let charIndex = 0; charIndex < this.textBuffer.length; charIndex++) { - const char = this.textBuffer[charIndex]; - - if (char !== '\n') { - continue; - } - - const line = this.textBuffer.slice(lineStart, charIndex); - lineStart = charIndex + 1; - - lines.push(line); - } - this.textBuffer = this.textBuffer.slice(lineStart); - - return lines; - } -} - -export { LinesDecoder }; diff --git a/src/util/ndjson/readNdJsonStream.ts b/src/util/ndjson/readNdJsonStream.ts deleted file mode 100644 index a2ae227..0000000 --- a/src/util/ndjson/readNdJsonStream.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Readable } from 'node:stream'; -import { CanceledError } from 'axios'; -import StreamToAsyncIterator from 'stream-to-async-iterator'; -import type { UnknownObject } from '../UnknownObject.js'; -import { CancelationError } from '../error/CancelationError.js'; -import { ServerError } from '../error/ServerError.js'; -import { LinesDecoder } from './LinesDecoder.js'; - -const readNdJsonStream = async function* ( - stream: Readable, -): AsyncGenerator { - const decoder = new LinesDecoder('utf-8'); - - try { - for await (const chunk of new StreamToAsyncIterator(stream)) { - const lines = decoder.write(chunk); - - for (const line of lines) { - const parsedLine = JSON.parse(line); - - yield parsedLine; - } - } - } catch (ex: unknown) { - if (ex instanceof CanceledError) { - throw new CancelationError(); - } - - throw new ServerError('Failed to read response.'); - } -}; - -export { readNdJsonStream }; diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile new file mode 100644 index 0000000..bb06067 --- /dev/null +++ b/test/docker/Dockerfile @@ -0,0 +1 @@ +FROM thenativeweb/eventsourcingdb:0.111.1 diff --git a/test/docker/EventSourcingDb.ts b/test/docker/EventSourcingDb.ts new file mode 100644 index 0000000..59cb8f6 --- /dev/null +++ b/test/docker/EventSourcingDb.ts @@ -0,0 +1,67 @@ +import { execSync } from 'node:child_process'; +import { setTimeout } from 'node:timers/promises'; +import getPort from 'get-port'; +import { Client } from '../../src/Client.js'; + +const imageName = 'thenativeweb/eventsourcingdb-test:latest'; + +class EventSourcingDb { + readonly #id: string; + readonly #port: number; + readonly #apiToken: string; + + public get port(): number { + return this.#port; + } + + public get apiToken(): string { + return this.#apiToken; + } + + private constructor(id: string, port: number, apiToken: string) { + this.#id = id; + this.#port = port; + this.#apiToken = apiToken; + } + + public static build(): void { + const cmd = `docker build -t ${imageName} .`; + execSync(cmd, { cwd: __dirname }); + } + + public static async run(): Promise { + const port = await getPort(); + const apiToken = 'secret'; + + const cmd = `docker run --detach --init --publish ${port}:3000 ${imageName} run --api-token ${apiToken} --data-directory-temporary --http-enabled --https-enabled=false`; + + const stdout = execSync(cmd, { cwd: __dirname }); + const id = stdout.toString('utf8').trim(); + + const client = new Client(new URL(`http://localhost:${port}`), apiToken); + + let isRunning = false; + for (let i = 0; i < 50; i++) { + try { + await client.ping(); + isRunning = true; + break; + } catch { + // Intentionally ignore error. + await setTimeout(100); + } + } + if (!isRunning) { + throw new Error('Failed to start Docker container.'); + } + + return new EventSourcingDb(id, port, apiToken); + } + + public kill(): void { + const cmd = `docker kill ${this.#id}`; + execSync(cmd, { cwd: __dirname }); + } +} + +export { EventSourcingDb }; diff --git a/test/integration/observeEventsTests.ts b/test/integration/observeEventsTests.ts deleted file mode 100644 index c2179a9..0000000 --- a/test/integration/observeEventsTests.ts +++ /dev/null @@ -1,767 +0,0 @@ -import assert from 'node:assert/strict'; -import { afterEach, before, beforeEach, suite, test } from 'node:test'; -import { ReasonPhrases, StatusCodes } from 'http-status-codes'; -import type { Client, StoreItem } from '../../src/index.js'; -import { CancelationError, Source } from '../../src/index.js'; -import { ClientError } from '../../src/util/error/ClientError.js'; -import { InvalidParameterError } from '../../src/util/error/InvalidParameterError.js'; -import { ServerError } from '../../src/util/error/ServerError.js'; -import type { Database } from '../shared/Database.js'; -import { newAbortControllerWithDeadline } from '../shared/abortController/newAbortControllerWithDeadline.js'; -import { buildDatabase } from '../shared/buildDatabase.js'; -import { events } from '../shared/events/events.js'; -import { testSource } from '../shared/events/source.js'; -import { startDatabase } from '../shared/startDatabase.js'; -import { startLocalHttpServer } from '../shared/startLocalHttpServer.js'; -import { stopDatabase } from '../shared/stopDatabase.js'; - -suite('observeEvents', { timeout: 20_000 }, () => { - let database: Database; - const source = new Source(testSource); - const testDeadline = 10_000; - - before(() => { - buildDatabase('test/shared/docker/eventsourcingdb'); - }); - - beforeEach(async () => { - database = await startDatabase(); - - await database.withAuthorization.client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - events.registered.janeDoe.traceParent, - ), - source.newEvent( - '/users/loggedIn', - events.loggedIn.janeDoe.type, - events.loggedIn.janeDoe.data, - events.loggedIn.janeDoe.traceParent, - ), - source.newEvent( - '/users/registered', - events.registered.johnDoe.type, - events.registered.johnDoe.data, - events.registered.johnDoe.traceParent, - ), - source.newEvent( - '/users/loggedIn', - events.loggedIn.johnDoe.type, - events.loggedIn.johnDoe.data, - events.loggedIn.johnDoe.traceParent, - ), - ]); - }); - - afterEach(() => { - stopDatabase(database); - }); - - test('throws an error when trying to read from a non-reachable server.', async (): Promise => { - const client = database.withInvalidUrl.client; - - await assert.rejects( - async () => { - const result = client.observeEvents(new AbortController(), '/', { recursive: false }); - - for await (const _ of result) { - // Do nothing. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: No response received.'); - return true; - }, - ); - }); - - test('throws an error if the subject is invalid.', async (): Promise => { - const client = database.withInvalidUrl.client; - - await assert.rejects( - async () => { - const result = client.observeEvents(new AbortController(), 'applepie', { - recursive: false, - }); - - for await (const _ of result) { - // Do nothing. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'subject' is invalid: Failed to validate subject: 'applepie' must be an absolute, slash-separated path.", - ); - return true; - }, - ); - }); - - test('supports authorization.', async (): Promise => { - const client = database.withAuthorization.client; - const abortController = newAbortControllerWithDeadline(testDeadline); - - // Should not throw. - let observedItemsCount = 0; - const result = client.observeEvents(abortController, '/', { recursive: true }); - - for await (const _data of result) { - observedItemsCount += 1; - - if (observedItemsCount === 4) { - return; - } - } - }); - - test('observes events from a single subject.', async (): Promise => { - const client = database.withAuthorization.client; - const abortController = newAbortControllerWithDeadline(testDeadline); - - const observedItems: StoreItem[] = []; - - // Should not throw. - let didPushIntermediateEvent = false; - const result = client.observeEvents(abortController, '/users/registered', { - recursive: false, - }); - - for await (const item of result) { - observedItems.push(item); - - if (!didPushIntermediateEvent) { - await client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.apfelFred.type, - events.registered.apfelFred.data, - events.registered.apfelFred.traceParent, - ), - ]); - - didPushIntermediateEvent = true; - } - - if (observedItems.length === 3) { - return; - } - } - - assert.equal(observedItems.length, 3); - assert.equal(observedItems[0].event.source, testSource); - assert.equal(observedItems[0].event.subject, '/users/registered'); - assert.equal(observedItems[0].event.type, events.registered.janeDoe.type); - assert.equal(observedItems[0].event.data, events.registered.janeDoe.data); - assert.equal(observedItems[0].event.traceParent, events.registered.janeDoe.traceParent); - assert.equal(observedItems[1].event.source, testSource); - assert.equal(observedItems[1].event.subject, '/users/registered'); - assert.equal(observedItems[1].event.type, events.registered.johnDoe.type); - assert.equal(observedItems[1].event.data, events.registered.johnDoe.data); - assert.equal(observedItems[1].event.traceParent, events.registered.johnDoe.traceParent); - assert.equal(observedItems[2].event.source, testSource); - assert.equal(observedItems[2].event.subject, '/users/registered'); - assert.equal(observedItems[2].event.type, events.registered.apfelFred.type); - assert.equal(observedItems[2].event.data, events.registered.apfelFred.data); - assert.equal(observedItems[2].event.traceParent, events.registered.apfelFred.traceParent); - }); - - test('observes events from a subject including child subjects.', async (): Promise => { - const client = database.withAuthorization.client; - const abortController = newAbortControllerWithDeadline(testDeadline); - - const observedItems: StoreItem[] = []; - - // Should not throw. - let didPushIntermediateEvent = false; - const result = client.observeEvents(abortController, '/users', { - recursive: true, - }); - - for await (const item of result) { - observedItems.push(item); - - if (!didPushIntermediateEvent) { - await client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.apfelFred.type, - events.registered.apfelFred.data, - ), - ]); - - didPushIntermediateEvent = true; - } - - if (observedItems.length === 5) { - return; - } - } - - assert.equal(observedItems.length, 5); - assert.equal(observedItems[0].event.source, testSource); - assert.equal(observedItems[0].event.subject, '/users/registered'); - assert.equal(observedItems[0].event.type, events.registered.janeDoe.type); - assert.equal(observedItems[0].event.data, events.registered.janeDoe.data); - assert.equal(observedItems[1].event.source, testSource); - assert.equal(observedItems[1].event.subject, '/users/loggedIn'); - assert.equal(observedItems[1].event.type, events.loggedIn.janeDoe.type); - assert.equal(observedItems[1].event.data, events.loggedIn.janeDoe.data); - assert.equal(observedItems[2].event.source, testSource); - assert.equal(observedItems[2].event.subject, '/users/registered'); - assert.equal(observedItems[2].event.type, events.registered.johnDoe.type); - assert.equal(observedItems[2].event.data, events.registered.johnDoe.data); - assert.equal(observedItems[3].event.source, testSource); - assert.equal(observedItems[3].event.subject, '/users/loggedIn'); - assert.equal(observedItems[3].event.type, events.loggedIn.johnDoe.type); - assert.equal(observedItems[3].event.data, events.loggedIn.johnDoe.data); - assert.equal(observedItems[4].event.source, testSource); - assert.equal(observedItems[4].event.subject, '/users/registered'); - assert.equal(observedItems[4].event.type, events.registered.apfelFred.type); - assert.equal(observedItems[4].event.data, events.registered.apfelFred.data); - }); - - test('observes events starting from the newest event matching the given event name.', async (): Promise => { - const client = database.withAuthorization.client; - const abortController = newAbortControllerWithDeadline(testDeadline); - - const observedItems: StoreItem[] = []; - // Should not throw. - let didPushIntermediateEvent = false; - const result = client.observeEvents(abortController, '/users', { - recursive: true, - fromLatestEvent: { - subject: '/users/loggedIn', - type: events.loggedIn.janeDoe.type, - ifEventIsMissing: 'read-everything', - }, - }); - - for await (const item of result) { - observedItems.push(item); - - if (!didPushIntermediateEvent) { - await client.writeEvents([ - source.newEvent( - '/users/loggedIn', - events.loggedIn.apfelFred.type, - events.loggedIn.apfelFred.data, - ), - ]); - - didPushIntermediateEvent = true; - } - - if (observedItems.length === 2) { - return; - } - } - - assert.equal(observedItems.length, 2); - assert.equal(observedItems[0].event.source, testSource); - assert.equal(observedItems[0].event.subject, '/users/loggedIn'); - assert.equal(observedItems[0].event.type, events.loggedIn.johnDoe.type); - assert.equal(observedItems[0].event.data, events.loggedIn.johnDoe.data); - assert.equal(observedItems[1].event.source, testSource); - assert.equal(observedItems[1].event.subject, '/users/loggedIn'); - assert.equal(observedItems[1].event.type, events.loggedIn.apfelFred.type); - assert.equal(observedItems[1].event.data, events.loggedIn.apfelFred.data); - }); - - test('observes events starting from the lower bound ID.', async (): Promise => { - const client = database.withAuthorization.client; - const abortController = newAbortControllerWithDeadline(testDeadline); - - const observedItems: StoreItem[] = []; - // Should not throw. - let didPushIntermediateEvent = false; - const result = client.observeEvents(abortController, '/users', { - recursive: true, - lowerBoundId: '2', - }); - - for await (const item of result) { - observedItems.push(item); - - if (!didPushIntermediateEvent) { - await client.writeEvents([ - source.newEvent( - '/users/loggedIn', - events.loggedIn.apfelFred.type, - events.loggedIn.apfelFred.data, - ), - ]); - - didPushIntermediateEvent = true; - } - - if (observedItems.length === 3) { - return; - } - } - - assert.equal(observedItems.length, 3); - assert.equal(observedItems[0].event.source, testSource); - assert.equal(observedItems[0].event.subject, '/users/registered'); - assert.equal(observedItems[0].event.type, events.registered.johnDoe.type); - assert.equal(observedItems[0].event.data, events.registered.johnDoe.data); - assert.equal(observedItems[1].event.source, testSource); - assert.equal(observedItems[1].event.subject, '/users/loggedIn'); - assert.equal(observedItems[1].event.type, events.loggedIn.johnDoe.type); - assert.equal(observedItems[1].event.data, events.loggedIn.johnDoe.data); - assert.equal(observedItems[2].event.source, testSource); - assert.equal(observedItems[2].event.subject, '/users/loggedIn'); - assert.equal(observedItems[2].event.type, events.loggedIn.apfelFred.type); - assert.equal(observedItems[2].event.data, events.loggedIn.apfelFred.data); - }); - - test('throws an error when the AbortController is aborted.', async (): Promise => { - const abortController = new AbortController(); - const result = database.withAuthorization.client.observeEvents(abortController, '/', { - recursive: true, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - abortController.abort(); - } - }, - error => { - assert.ok(error instanceof CancelationError); - return true; - }, - ); - }); - - test('throws an error if mutually exclusive options are used.', async (): Promise => { - const result = database.withAuthorization.client.observeEvents( - new AbortController(), - '/users', - { - recursive: true, - fromLatestEvent: { - subject: '/', - type: 'com.foobar.barbaz', - ifEventIsMissing: 'wait-for-event', - }, - lowerBoundId: '2', - }, - ); - - await assert.rejects( - async () => { - for await (const _ of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ObserveEventsOptions are invalid: lowerBoundId and fromLatestEvent are mutually exclusive.", - ); - return true; - }, - ); - }); - - test('throws an error if the given lowerBoundId does not contain an integer', async () => { - const result = database.withAuthorization.client.observeEvents( - new AbortController(), - '/users', - { - recursive: true, - lowerBoundId: 'some-id', - }, - ); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ObserveEventsOptions are invalid: lowerBoundId must be 0 or greater.", - ); - return true; - }, - ); - }); - - test('throws an error if the given lowerBoundId does not contain a negative integer', async () => { - const result = database.withAuthorization.client.observeEvents( - new AbortController(), - '/users', - { - recursive: true, - lowerBoundId: '-3', - }, - ); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ObserveEventsOptions are invalid: lowerBoundId must be 0 or greater.", - ); - return true; - }, - ); - }); - - test('throws an error if an incorrect subject is used in fromLatestEvent.', async () => { - const result = database.withAuthorization.client.observeEvents( - new AbortController(), - '/users', - { - recursive: true, - fromLatestEvent: { - type: 'com.some.type', - subject: 'this is wrong', - ifEventIsMissing: 'wait-for-event', - }, - }, - ); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ObserveEventsOptions are invalid: Failed to validate 'fromLatestEvent': Failed to validate subject: 'this is wrong' must be an absolute, slash-separated path.", - ); - return true; - }, - ); - }); - - test('throws an error if an incorrect type is used in fromLatestEvent.', async () => { - const result = database.withAuthorization.client.observeEvents( - new AbortController(), - '/users', - { - recursive: true, - fromLatestEvent: { - type: 'this is wrong', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }, - ); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ObserveEventsOptions are invalid: Failed to validate 'fromLatestEvent': Failed to validate type: 'this is wrong' must be a reverse domain name.", - ); - return true; - }, - ); - }); - - suite('using a mock server', () => { - let stopServer: () => Promise; - - afterEach(async () => { - await stopServer(); - }); - - test('throws a server error if the server responds with http 5xx on every try.', async () => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.status(StatusCodes.BAD_GATEWAY); - res.send(ReasonPhrases.BAD_GATEWAY); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - "Server error occurred: Request failed with status code '502'.", - ); - return true; - }, - ); - }); - - test("throws an error if the server's protocol version does not match.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.setHeader('X-EventSourcingDB-Protocol-Version', '0.0.0'); - res.status(StatusCodes.UNPROCESSABLE_ENTITY); - res.send(ReasonPhrases.UNPROCESSABLE_ENTITY); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Protocol version mismatch, server '0.0.0', client '1.0.0.'", - ); - return true; - }, - ); - }); - - test('throws a client error if the server returns a 4xx status code.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.status(StatusCodes.IM_A_TEAPOT); - res.send(ReasonPhrases.IM_A_TEAPOT); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Request failed with status code '418'.", - ); - return true; - }, - ); - }); - - test('returns a server error if the server returns a non 200, 5xx or 4xx status code.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.status(StatusCodes.ACCEPTED); - res.send(ReasonPhrases.ACCEPTED); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Unexpected response status: 202 Accepted.', - ); - return true; - }, - ); - }); - - test("throws a server error if the server sends a stream item that can't be unmarshalled.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.send('utter garbage\n'); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: Failed to read response.'); - return true; - }, - ); - }); - - test('throws a server error if the server sends a stream item with unsupported type.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.send('{"type": ":clown:"}\n'); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Failed to observe events, an unexpected stream item was received: \'{"type":":clown:"}\'.', - ); - return true; - }, - ); - }); - - test('throws a server error if the server sends a an error item through the ndjson stream.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.send('{"type": "error", "payload": { "error": "not enough JUICE 😩" }}\n'); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: not enough JUICE 😩.'); - return true; - }, - ); - }); - - test("throws a server error if the server sends a an error item through the ndjson stream, but the error can't be unmarshalled.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/observe-events', (_req, res) => { - res.send('{"type": "error", "payload": 42}\n'); - }); - })); - - const result = client.observeEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'wait-for-event', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert(error instanceof ServerError); - assert( - error.message, - 'Server error occurred: Failed to observe events, an unexpected stream item was received: \'{"type":"error","payload":42}\'.', - ); - return true; - }, - ); - }); - }); -}); diff --git a/test/integration/pingTests.ts b/test/integration/pingTests.ts deleted file mode 100644 index da1f89a..0000000 --- a/test/integration/pingTests.ts +++ /dev/null @@ -1,101 +0,0 @@ -import assert from 'node:assert/strict'; -import { afterEach, before, beforeEach, suite, test } from 'node:test'; -import { StatusCodes } from 'http-status-codes'; -import type { Client } from '../../src/index.js'; -import { ServerError } from '../../src/util/error/ServerError.js'; -import type { Database } from '../shared/Database.js'; -import { buildDatabase } from '../shared/buildDatabase.js'; -import { startDatabase } from '../shared/startDatabase.js'; -import { startLocalHttpServer } from '../shared/startLocalHttpServer.js'; -import { stopDatabase } from '../shared/stopDatabase.js'; - -suite('ping', { timeout: 20_000 }, () => { - let database: Database; - - before(() => { - buildDatabase('test/shared/docker/eventsourcingdb'); - }); - - beforeEach(async () => { - database = await startDatabase(); - }); - - afterEach(() => { - stopDatabase(database); - }); - - test('throws an error if the server is not reachable.', async (): Promise => { - const client = database.withInvalidUrl.client; - - await assert.rejects( - async () => { - await client.ping(); - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: No response received.'); - return true; - }, - ); - }); - - test('does not throw an error if EventSourcingDB is reachable.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.ping(); - }); - - suite('with a mock server', () => { - let stopServer: () => Promise; - - afterEach(async () => { - await stopServer(); - }); - - test('throws an error if the server responds with an unexpected status code.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.get('/api/ping', (_req, res) => { - res.status(StatusCodes.BAD_GATEWAY); - res.send('OK'); - }); - })); - - await assert.rejects( - async () => { - await client.ping(); - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - "Server error occurred: Request failed with status code '502'.", - ); - return true; - }, - ); - }); - - test("throws an error if the server's response body is not 'OK'.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.get('/api/ping', (_req, res) => { - res.status(StatusCodes.OK); - res.send('Gude'); - }); - })); - - await assert.rejects( - async () => { - await client.ping(); - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: Received unexpected response.'); - return true; - }, - ); - }); - }); -}); diff --git a/test/integration/readEventTypesTests.ts b/test/integration/readEventTypesTests.ts deleted file mode 100644 index a85cdd5..0000000 --- a/test/integration/readEventTypesTests.ts +++ /dev/null @@ -1,76 +0,0 @@ -import assert from 'node:assert/strict'; -import { afterEach, before, beforeEach, suite, test } from 'node:test'; -import type { EventType } from '../../src/handlers/readEventTypes/EventType.js'; -import { EventCandidate } from '../../src/index.js'; -import type { Database } from '../shared/Database.js'; -import { buildDatabase } from '../shared/buildDatabase.js'; -import { testSource } from '../shared/events/source.js'; -import { startDatabase } from '../shared/startDatabase.js'; -import { stopDatabase } from '../shared/stopDatabase.js'; - -suite('readEventTypes', { timeout: 20_000 }, () => { - let database: Database; - - before(() => { - buildDatabase('test/shared/docker/eventsourcingdb'); - }); - - beforeEach(async () => { - database = await startDatabase(); - }); - - afterEach(() => { - stopDatabase(database); - }); - - test('Reads all event types of existing events, as well as all event types with registered schemas.', async () => { - const client = database.withAuthorization.client; - - await client.writeEvents([ - new EventCandidate(testSource, '/account', 'com.foo.bar', {}), - new EventCandidate(testSource, '/account/user', 'com.bar.baz', {}), - new EventCandidate(testSource, '/account/user', 'com.baz.leml', {}), - new EventCandidate(testSource, '/', 'com.quux.knax', {}), - ]); - - await client.registerEventSchema('org.ban.ban', { type: 'object' }); - await client.registerEventSchema('org.bing.chilling', { type: 'object' }); - - const expectedEventTypes: EventType[] = [ - { - eventType: 'com.bar.baz', - isPhantom: false, - }, - { - eventType: 'com.baz.leml', - isPhantom: false, - }, - { - eventType: 'com.foo.bar', - isPhantom: false, - }, - { - eventType: 'com.quux.knax', - isPhantom: false, - }, - { - eventType: 'org.ban.ban', - isPhantom: true, - schema: `{"type":"object"}`, - }, - { - eventType: 'org.bing.chilling', - isPhantom: true, - schema: `{"type":"object"}`, - }, - ]; - - const observedEventTypes: EventType[] = []; - for await (const observedEventType of client.readEventTypes(new AbortController())) { - observedEventTypes.push(observedEventType); - } - - assert.deepEqual(observedEventTypes, expectedEventTypes); - assert.equal(observedEventTypes.length, expectedEventTypes.length); - }); -}); diff --git a/test/integration/readEventsTests.ts b/test/integration/readEventsTests.ts deleted file mode 100644 index 0dfba27..0000000 --- a/test/integration/readEventsTests.ts +++ /dev/null @@ -1,728 +0,0 @@ -import assert from 'node:assert/strict'; -import { afterEach, before, beforeEach, suite, test } from 'node:test'; -import { ReasonPhrases, StatusCodes } from 'http-status-codes'; -import type { Client, StoreItem } from '../../src/index.js'; -import { CancelationError, Source } from '../../src/index.js'; -import { ClientError } from '../../src/util/error/ClientError.js'; -import { InvalidParameterError } from '../../src/util/error/InvalidParameterError.js'; -import { ServerError } from '../../src/util/error/ServerError.js'; -import type { Database } from '../shared/Database.js'; -import { buildDatabase } from '../shared/buildDatabase.js'; -import { events } from '../shared/events/events.js'; -import { testSource } from '../shared/events/source.js'; -import { startDatabase } from '../shared/startDatabase.js'; -import { startLocalHttpServer } from '../shared/startLocalHttpServer.js'; -import { stopDatabase } from '../shared/stopDatabase.js'; - -suite('readEvents', { timeout: 20_000 }, () => { - let database: Database; - const source = new Source(testSource); - - before(() => { - buildDatabase('test/shared/docker/eventsourcingdb'); - }); - - beforeEach(async () => { - database = await startDatabase(); - - await database.withAuthorization.client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - events.registered.janeDoe.traceParent, - ), - source.newEvent( - '/users/loggedIn', - events.loggedIn.janeDoe.type, - events.loggedIn.janeDoe.data, - events.loggedIn.janeDoe.traceParent, - ), - source.newEvent( - '/users/registered', - events.registered.johnDoe.type, - events.registered.johnDoe.data, - events.registered.johnDoe.traceParent, - ), - source.newEvent( - '/users/loggedIn', - events.loggedIn.johnDoe.type, - events.loggedIn.johnDoe.data, - events.loggedIn.johnDoe.traceParent, - ), - ]); - }); - - afterEach(() => { - stopDatabase(database); - }); - - test('throws an error when trying to read from a non-reachable server.', async (): Promise => { - const client = database.withInvalidUrl.client; - - await assert.rejects(async () => { - const result = client.readEvents(new AbortController(), '/', { recursive: false }); - - for await (const _ of result) { - // Do nothing. - } - }); - }); - - test('supports authorization.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - const result = client.readEvents(new AbortController(), '/', { recursive: false }); - - for await (const _ of result) { - // Do nothing. - } - }); - - test('reads events from a single subject.', async (): Promise => { - const result = database.withAuthorization.client.readEvents( - new AbortController(), - '/users/registered', - { recursive: false }, - ); - - const readItems: StoreItem[] = []; - for await (const item of result) { - readItems.push(item); - } - - assert.equal(readItems.length, 2); - - assert.equal(readItems[0].event.source, testSource); - assert.equal(readItems[0].event.subject, '/users/registered'); - assert.equal(readItems[0].event.type, events.registered.janeDoe.type); - assert.deepEqual(readItems[0].event.data, events.registered.janeDoe.data); - assert.equal(readItems[0].event.traceParent, events.registered.janeDoe.traceParent); - - assert.equal(readItems[1].event.source, testSource); - assert.equal(readItems[1].event.subject, '/users/registered'); - assert.equal(readItems[1].event.type, events.registered.johnDoe.type); - assert.deepEqual(readItems[1].event.data, events.registered.johnDoe.data); - assert.equal(readItems[1].event.traceParent, events.registered.johnDoe.traceParent); - }); - - test('reads events from a subject including child subjects.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - }); - - const readItems: StoreItem[] = []; - for await (const item of result) { - readItems.push(item); - } - - assert.equal(readItems.length, 4); - - assert.equal(readItems[0].event.source, testSource); - assert.equal(readItems[0].event.subject, '/users/registered'); - assert.equal(readItems[0].event.type, events.registered.janeDoe.type); - assert.deepEqual(readItems[0].event.data, events.registered.janeDoe.data); - - assert.equal(readItems[1].event.source, testSource); - assert.equal(readItems[1].event.subject, '/users/loggedIn'); - assert.equal(readItems[1].event.type, events.loggedIn.janeDoe.type); - assert.deepEqual(readItems[1].event.data, events.loggedIn.janeDoe.data); - - assert.equal(readItems[2].event.source, testSource); - assert.equal(readItems[2].event.subject, '/users/registered'); - assert.equal(readItems[2].event.type, events.registered.johnDoe.type); - assert.deepEqual(readItems[2].event.data, events.registered.johnDoe.data); - - assert.equal(readItems[3].event.source, testSource); - assert.equal(readItems[3].event.subject, '/users/loggedIn'); - assert.equal(readItems[3].event.type, events.loggedIn.johnDoe.type); - assert.deepEqual(readItems[3].event.data, events.loggedIn.johnDoe.data); - }); - - test('reads the events in antichronological order.', async (): Promise => { - const result = database.withAuthorization.client.readEvents( - new AbortController(), - '/users/registered', - { recursive: false, order: 'antichronological' }, - ); - - const readItems: StoreItem[] = []; - for await (const item of result) { - readItems.push(item); - } - - assert.equal(readItems.length, 2); - - assert.equal(readItems[0].event.source, testSource); - assert.equal(readItems[0].event.subject, '/users/registered'); - assert.equal(readItems[0].event.type, events.registered.johnDoe.type); - assert.deepEqual(readItems[0].event.data, events.registered.johnDoe.data); - - assert.equal(readItems[1].event.source, testSource); - assert.equal(readItems[1].event.subject, '/users/registered'); - assert.equal(readItems[1].event.type, events.registered.janeDoe.type); - assert.deepEqual(readItems[1].event.data, events.registered.janeDoe.data); - }); - - test('reads events starting from the latest event matching the given event name.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - subject: '/users/loggedIn', - type: events.loggedIn.janeDoe.type, - ifEventIsMissing: 'read-everything', - }, - }); - - const readItems: StoreItem[] = []; - for await (const item of result) { - readItems.push(item); - } - - assert.equal(readItems.length, 1); - - assert.equal(readItems[0].event.source, testSource); - assert.equal(readItems[0].event.subject, '/users/loggedIn'); - assert.equal(readItems[0].event.type, events.loggedIn.johnDoe.type); - assert.deepEqual(readItems[0].event.data, events.loggedIn.johnDoe.data); - }); - - test('reads events starting from the lower bound ID.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - lowerBoundId: '2', - }); - - const readItems: StoreItem[] = []; - for await (const item of result) { - readItems.push(item); - } - - assert.equal(readItems.length, 2); - - assert.equal(readItems[0].event.source, testSource); - assert.equal(readItems[0].event.subject, '/users/registered'); - assert.equal(readItems[0].event.type, events.registered.johnDoe.type); - assert.deepEqual(readItems[0].event.data, events.registered.johnDoe.data); - - assert.equal(readItems[1].event.source, testSource); - assert.equal(readItems[1].event.subject, '/users/loggedIn'); - assert.equal(readItems[1].event.type, events.loggedIn.johnDoe.type); - assert.deepEqual(readItems[1].event.data, events.loggedIn.johnDoe.data); - }); - - test('reads events up to the upper bound ID.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - upperBoundId: '1', - }); - - const readItems: StoreItem[] = []; - for await (const item of result) { - readItems.push(item); - } - - assert.equal(readItems.length, 2); - - assert.equal(readItems[0].event.source, testSource); - assert.equal(readItems[0].event.subject, '/users/registered'); - assert.equal(readItems[0].event.type, events.registered.janeDoe.type); - assert.deepEqual(readItems[0].event.data, events.registered.janeDoe.data); - - assert.equal(readItems[1].event.source, testSource); - assert.equal(readItems[1].event.subject, '/users/loggedIn'); - assert.equal(readItems[1].event.type, events.loggedIn.janeDoe.type); - assert.deepEqual(readItems[1].event.data, events.loggedIn.janeDoe.data); - }); - - test('throws an error when the AbortController is aborted.', async (): Promise => { - const abortController = new AbortController(); - const result = database.withAuthorization.client.readEvents(abortController, '/users', { - recursive: true, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - abortController.abort(); - } - }, - error => { - assert.ok(error instanceof CancelationError); - return true; - }, - ); - }); - - test('throws an error if mutually exclusive options are used.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - subject: '/', - type: 'com.foobar.barbaz', - ifEventIsMissing: 'read-everything', - }, - lowerBoundId: '2', - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - { - message: - "Parameter 'options' is invalid: ReadEventsOptions are invalid: lowerBoundId and fromLatestEvent are mutually exclusive.", - }, - ); - }); - - test('throws an error if the subject is invalid.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), 'invalid', { - recursive: true, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'subject' is invalid: Failed to validate subject: 'invalid' must be an absolute, slash-separated path.", - ); - return true; - }, - ); - }); - - test('throws an error if the given lowerBoundID does not contain an integer.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - lowerBoundId: 'invalid', - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ReadEventsOptions are invalid: lowerBoundId must be 0 or greater.", - ); - return true; - }, - ); - }); - - test('throws an error if the given lowerBoundID contains an integer that is negative.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - lowerBoundId: '-1', - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ReadEventsOptions are invalid: lowerBoundId must be 0 or greater.", - ); - return true; - }, - ); - }); - - test('throws an error if the given upperBoundID does not contain an integer.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - upperBoundId: 'invalid', - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ReadEventsOptions are invalid: upperBoundId must be 0 or greater.", - ); - return true; - }, - ); - }); - - test('throws an error if the given upperBoundID contains an integer that is negative.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - upperBoundId: '-1', - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ReadEventsOptions are invalid: upperBoundId must be 0 or greater.", - ); - return true; - }, - ); - }); - - test('throws an error if an incorrect subject is used in ReadFromLatestEvent.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - subject: 'invalid', - type: 'com.foo.bar', - ifEventIsMissing: 'read-everything', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ReadEventsOptions are invalid: Failed to validate 'fromLatestEvent': Failed to validate subject: 'invalid' must be an absolute, slash-separated path.", - ); - return true; - }, - ); - }); - - test('throws an error if an incorrect type is used in ReadFromLatestEvent.', async (): Promise => { - const result = database.withAuthorization.client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - subject: '/users', - type: 'invalid', - ifEventIsMissing: 'read-everything', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'options' is invalid: ReadEventsOptions are invalid: Failed to validate 'fromLatestEvent': Failed to validate type: 'invalid' must be a reverse domain name.", - ); - return true; - }, - ); - }); - - suite('using a mock server', () => { - let stopServer: () => Promise; - - afterEach(async () => { - await stopServer(); - }); - - test('throws a sever error if the server responds with HTTP 5xx on every try.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.status(StatusCodes.BAD_GATEWAY); - res.send(ReasonPhrases.BAD_GATEWAY); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - "Server error occurred: Request failed with status code '502'.", - ); - return true; - }, - ); - }); - - test("throws an error if the server's protocol version does not match.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.setHeader('X-EventSourcingDB-Protocol-Version', '0.0.0'); - res.status(StatusCodes.UNPROCESSABLE_ENTITY); - res.send(ReasonPhrases.UNPROCESSABLE_ENTITY); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Protocol version mismatch, server '0.0.0', client '1.0.0.'", - ); - return true; - }, - ); - }); - - test('throws a client error if the server returns a 4xx status code.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.status(StatusCodes.IM_A_TEAPOT); - res.send(ReasonPhrases.IM_A_TEAPOT); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Request failed with status code '418'.", - ); - return true; - }, - ); - }); - - test('throws a server error if the server returns a non 200, 5xx or 4xx status code.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.status(StatusCodes.ACCEPTED); - res.send(ReasonPhrases.ACCEPTED); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Unexpected response status: 202 Accepted.', - ); - return true; - }, - ); - }); - - test("throws a server error if the server sends a stream item that can't be unmarshalled.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.send('utter garbage\n'); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: Failed to read response.'); - return true; - }, - ); - }); - - test('throws a server error if the server sends a stream item with unsupported type.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.send('{"type": ":clown:"}\n'); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Failed to read events, an unexpected stream item was received: \'{"type":":clown:"}\'.', - ); - return true; - }, - ); - }); - - test('throws a server error if the server sends a an error item through the ndjson stream.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.send('{"type": "error", "payload": { "error": "not enough JUICE 😩" }}\n'); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: not enough JUICE 😩.'); - return true; - }, - ); - }); - - test("throws a server error if the server sends a an error item through the ndjson stream, but the error can't be unmarshalled.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-events', (_req, res) => { - res.send('{"type": "error", "payload": 42}\n'); - }); - })); - - const result = client.readEvents(new AbortController(), '/users', { - recursive: true, - fromLatestEvent: { - type: 'com.subject.some', - subject: '/some/subject', - ifEventIsMissing: 'read-nothing', - }, - }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Failed to read events, an unexpected stream item was received: \'{"type":"error","payload":42}\'.', - ); - return true; - }, - ); - }); - }); -}); diff --git a/test/integration/readSubjectsTests.ts b/test/integration/readSubjectsTests.ts deleted file mode 100644 index b35e484..0000000 --- a/test/integration/readSubjectsTests.ts +++ /dev/null @@ -1,350 +0,0 @@ -import assert from 'node:assert/strict'; -import { afterEach, before, beforeEach, suite, test } from 'node:test'; -import { ReasonPhrases, StatusCodes } from 'http-status-codes'; -import type { Client } from '../../src/index.js'; -import { CancelationError, EventCandidate } from '../../src/index.js'; -import { ClientError } from '../../src/util/error/ClientError.js'; -import { ServerError } from '../../src/util/error/ServerError.js'; -import type { Database } from '../shared/Database.js'; -import { buildDatabase } from '../shared/buildDatabase.js'; -import { events } from '../shared/events/events.js'; -import { testSource } from '../shared/events/source.js'; -import { startDatabase } from '../shared/startDatabase.js'; -import { startLocalHttpServer } from '../shared/startLocalHttpServer.js'; -import { stopDatabase } from '../shared/stopDatabase.js'; - -suite('readSubjects', { timeout: 20_000 }, () => { - let database: Database; - - before(() => { - buildDatabase('test/shared/docker/eventsourcingdb'); - }); - - beforeEach(async () => { - database = await startDatabase(); - }); - - afterEach(() => { - stopDatabase(database); - }); - - test('throws an error when trying to read from a non-reachable server.', async (): Promise => { - const client = database.withInvalidUrl.client; - - await assert.rejects(async () => { - const readSubjectsResult = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - for await (const _ of readSubjectsResult) { - // Intentionally left blank. - } - }); - }); - - test('supports authorization.', async (): Promise => { - const client = database.withAuthorization.client; - // Should not throw. - const readSubjectsResult = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - for await (const _ of readSubjectsResult) { - // Intentionally left blank. - } - }); - - test('reads all subjects starting from /.', async (): Promise => { - const client = database.withAuthorization.client; - - await client.writeEvents([ - new EventCandidate(testSource, '/foo', events.loggedIn.janeDoe.type, {}), - ]); - - const actualSubjects: string[] = []; - - const readSubjectsResult = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - for await (const subject of readSubjectsResult) { - actualSubjects.push(subject); - } - - assert.deepEqual(actualSubjects, ['/', '/foo']); - }); - - test('reads all subjects starting from the given base subject.', async (): Promise => { - const client = database.withAuthorization.client; - - await client.writeEvents([ - new EventCandidate(testSource, '/foo/bar', events.loggedIn.janeDoe.type, {}), - ]); - - const actualSubjects: string[] = []; - - const readSubjectsResult = client.readSubjects(new AbortController(), { baseSubject: '/foo' }); - - for await (const subject of readSubjectsResult) { - actualSubjects.push(subject); - } - - assert.deepEqual(actualSubjects, ['/foo', '/foo/bar']); - }); - - test('throws an error when the AbortController is aborted.', async (): Promise => { - const client = database.withAuthorization.client; - - const abortController = new AbortController(); - - await assert.rejects( - async () => { - const readSubjectsResult = client.readSubjects(abortController, { baseSubject: '/' }); - abortController.abort(); - - for await (const _ of readSubjectsResult) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof CancelationError); - return true; - }, - ); - }); - - test('throws an error if the base subject is malformed.', async (): Promise => { - const client = database.withAuthorization.client; - - await assert.rejects( - async () => { - const readSubjectsResult = client.readSubjects(new AbortController(), { baseSubject: '' }); - - for await (const _ of readSubjectsResult) { - // Intentionally left blank. - } - }, - { - message: - "Parameter 'options' is invalid: Failed to validate subject: '' must be an absolute, slash-separated path.", - }, - ); - }); - - suite('with a mock server', () => { - let stopServer: () => Promise; - - afterEach(async () => { - await stopServer(); - }); - - test('throws a server error if the server responds with http 5xx on every try.', async () => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.status(StatusCodes.BAD_GATEWAY); - res.send(ReasonPhrases.BAD_GATEWAY); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - "Server error occurred: Request failed with status code '502'.", - ); - return true; - }, - ); - }); - - test("throws an error if the server's protocol version does not match.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.setHeader('X-EventSourcingDB-Protocol-Version', '0.0.0'); - res.status(StatusCodes.UNPROCESSABLE_ENTITY); - res.send(ReasonPhrases.UNPROCESSABLE_ENTITY); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Protocol version mismatch, server '0.0.0', client '1.0.0.'", - ); - return true; - }, - ); - }); - - test('throws a client error if the server returns a 4xx status code.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.status(StatusCodes.IM_A_TEAPOT); - res.send(ReasonPhrases.IM_A_TEAPOT); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Request failed with status code '418'.", - ); - return true; - }, - ); - }); - - test('returns a server error if the server returns a non 200, 5xx or 4xx status code.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.status(StatusCodes.ACCEPTED); - res.send(ReasonPhrases.ACCEPTED); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Unexpected response status: 202 Accepted.', - ); - return true; - }, - ); - }); - - test("throws a server error if the server sends a stream item that can't be unmarshalled.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.send('utter garbage\n'); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: Failed to read response.'); - return true; - }, - ); - }); - - test('throws a server error if the server sends a stream item with unsupported type.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.send('{"type": ":clown:"}\n'); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Failed to read subjects, an unexpected stream item was received: \'{"type":":clown:"}\'.', - ); - return true; - }, - ); - }); - - test('throws a server error if the server sends a an error item through the ndjson stream.', async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.send('{"type": "error", "payload": { "error": "not enough JUICE 😩." }}\n'); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: not enough JUICE 😩.'); - return true; - }, - ); - }); - - test("throws a server error if the server sends a an error item through the ndjson stream, but the error can't be unmarshalled.", async (): Promise => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/read-subjects', (_req, res) => { - res.send('{"type": "error", "payload": 42}\n'); - }); - })); - - const result = client.readSubjects(new AbortController(), { baseSubject: '/' }); - - await assert.rejects( - async () => { - for await (const _item of result) { - // Intentionally left blank. - } - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Failed to read subjects, an unexpected stream item was received: \'{"type":"error","payload":42}\'.', - ); - return true; - }, - ); - }); - }); -}); diff --git a/test/integration/registerEventSchemaTests.ts b/test/integration/registerEventSchemaTests.ts deleted file mode 100644 index 0a1ec49..0000000 --- a/test/integration/registerEventSchemaTests.ts +++ /dev/null @@ -1,83 +0,0 @@ -import assert from 'node:assert/strict'; -import { afterEach, before, beforeEach, suite, test } from 'node:test'; -import { EventCandidate } from '../../src/index.js'; -import type { Database } from '../shared/Database.js'; -import { buildDatabase } from '../shared/buildDatabase.js'; -import { testSource } from '../shared/events/source.js'; -import { startDatabase } from '../shared/startDatabase.js'; -import { stopDatabase } from '../shared/stopDatabase.js'; - -suite('registerEventSchema', { timeout: 20_000 }, () => { - let database: Database; - - before(() => { - buildDatabase('test/shared/docker/eventsourcingdb'); - }); - - beforeEach(async () => { - database = await startDatabase(); - }); - - afterEach(() => { - stopDatabase(database); - }); - - test("registers the new schema if it doesn't conflict with existing events.", async (): Promise => { - const client = database.withAuthorization.client; - - await client.writeEvents([new EventCandidate(testSource, '/eppes', 'com.ekht.ekht', {})]); - - await client.registerEventSchema('com.ekht.ekht', { type: 'object' }); - }); - - test('rejects the request if at least one of the existing events conflicts with the schema.', async (): Promise => { - const client = database.withAuthorization.client; - - await client.writeEvents([ - new EventCandidate(testSource, '/eppes', 'com.gornisht.ekht', { oy: 'gevalt' }), - ]); - - await assert.rejects(async () => { - await client.registerEventSchema('com.gornisht.ekht', { - type: 'object', - additionalProperties: false, - }); - }); - }); - - test('rejects the request if the schema already exists.', async (): Promise => { - const client = database.withAuthorization.client; - - await client.writeEvents([new EventCandidate(testSource, '/eppes', 'com.ekht.ekht', {})]); - - await client.registerEventSchema('com.ekht.ekht', { type: 'object' }); - - await assert.rejects(async () => { - await client.registerEventSchema('com.ekht.ekht', { type: 'object' }); - }); - }); - - test('rejects the request if the given schema is not valid JSON.', async (): Promise => { - const client = database.withAuthorization.client; - - await assert.rejects(async () => { - await client.registerEventSchema('com.ekht.ekht', '{"type":'); - }); - }); - - test('rejects the request if the given schema is not valid JSONSchema.', async (): Promise => { - const client = database.withAuthorization.client; - - await assert.rejects(async () => { - await client.registerEventSchema('com.ekht.ekht', { type: 'object', properties: 'banana' }); - }); - }); - - test('rejects the request if the given schema does not specify an object at the top level.', async (): Promise => { - const client = database.withAuthorization.client; - - await assert.rejects(async () => { - await client.registerEventSchema('com.ekht.ekht', { type: 'array' }); - }); - }); -}); diff --git a/test/integration/writeEventsTests.ts b/test/integration/writeEventsTests.ts deleted file mode 100644 index 986f4e1..0000000 --- a/test/integration/writeEventsTests.ts +++ /dev/null @@ -1,485 +0,0 @@ -import assert from 'node:assert/strict'; -import { afterEach, before, beforeEach, suite, test } from 'node:test'; -import { ReasonPhrases, StatusCodes } from 'http-status-codes'; -import { Source, isSubjectOnEventId, isSubjectPristine } from '../../src/index.js'; -import type { Client, StoreItem } from '../../src/index.js'; -import { ClientError } from '../../src/util/error/ClientError.js'; -import { InvalidParameterError } from '../../src/util/error/InvalidParameterError.js'; -import { ServerError } from '../../src/util/error/ServerError.js'; -import type { Database } from '../shared/Database.js'; -import { buildDatabase } from '../shared/buildDatabase.js'; -import { events } from '../shared/events/events.js'; -import { testSource } from '../shared/events/source.js'; -import { prefixEventType } from '../shared/events/type.js'; -import { startDatabase } from '../shared/startDatabase.js'; -import { startLocalHttpServer } from '../shared/startLocalHttpServer.js'; -import { stopDatabase } from '../shared/stopDatabase.js'; - -suite('writeEvents', { timeout: 20_000 }, () => { - let database: Database; - const source = new Source(testSource); - - before(() => { - buildDatabase('test/shared/docker/eventsourcingdb'); - }); - - beforeEach(async () => { - database = await startDatabase(); - }); - - afterEach(() => { - stopDatabase(database); - }); - - test('throws an error when trying to write to a non-reachable server.', async (): Promise => { - const client = database.withInvalidUrl.client; - - await assert.rejects( - async () => { - await client.writeEvents([ - source.newEvent( - '/foobar', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - ), - ]); - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal(error.message, 'Server error occurred: No response received.'); - return true; - }, - ); - }); - - test('throws an error if no candidates are passed.', async (): Promise => { - const client = database.withAuthorization.client; - - await assert.rejects( - async () => { - await client.writeEvents([]); - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'eventCandidates' is invalid: eventCandidates must contain at least one EventCandidate.", - ); - return true; - }, - ); - }); - - test('throws an error if a candidate subject is malformed.', async (): Promise => { - const client = database.withAuthorization.client; - - await assert.rejects( - async () => { - await client.writeEvents([ - source.newEvent('foobar', events.registered.janeDoe.type, events.registered.janeDoe.data), - ]); - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'eventCandidates' is invalid: Failed to validate subject: 'foobar' must be an absolute, slash-separated path.", - ); - return true; - }, - ); - }); - - test('throws an error if a candidate type is malformed.', async (): Promise => { - const client = database.withAuthorization.client; - - await assert.rejects( - async () => { - await client.writeEvents([ - source.newEvent('/foobar', 'haram', events.registered.janeDoe.data), - ]); - }, - error => { - assert.ok(error instanceof InvalidParameterError); - assert.equal( - error.message, - "Parameter 'eventCandidates' is invalid: Failed to validate type: 'haram' must be a reverse domain name.", - ); - return true; - }, - ); - }); - - test('throws an error if a precondition uses an invalid source.'); - - test('supports authorization.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents([ - source.newEvent('/foobar', events.registered.janeDoe.type, events.registered.janeDoe.data), - ]); - }); - - test('writes a single event.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents([ - source.newEvent( - '/foobar', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - '00-eb0e08452e7ee4b0d3b8b30987c37951-c31bc0a7013beab8-00', - ), - ]); - }); - - test('returns the written event metadata.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - ), - ]); - - const writtenEventsMetadata = await client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.johnDoe.type, - events.registered.johnDoe.data, - ), - source.newEvent( - '/users/loggedIn', - events.loggedIn.johnDoe.type, - events.loggedIn.johnDoe.data, - ), - ]); - - assert.equal(writtenEventsMetadata.length, 2); - assert.equal(writtenEventsMetadata[0].source, testSource); - assert.equal(writtenEventsMetadata[0].type, prefixEventType('registered')); - assert.equal(writtenEventsMetadata[0].subject, '/users/registered'); - assert.equal(writtenEventsMetadata[0].id, '1'); - assert.equal(writtenEventsMetadata[1].source, testSource); - assert.equal(writtenEventsMetadata[1].type, prefixEventType('loggedIn')); - assert.equal(writtenEventsMetadata[1].subject, '/users/loggedIn'); - assert.equal(writtenEventsMetadata[1].id, '2'); - }); - - test('writes multiple events.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - ), - source.newEvent( - '/users/registered', - events.registered.johnDoe.type, - events.registered.johnDoe.data, - ), - ]); - }); - - test('throws an error if any of the given events does not validate against the schema.', async (): Promise => { - const client = database.withAuthorization.client; - - await client.registerEventSchema('com.knackige.wuerstchen', { - type: 'object', - additionalProperties: false, - }); - - await assert.rejects( - async () => { - await client.writeEvents([ - source.newEvent('/users/registered', 'com.knackige.wuerstchen', { oh: 'no' }), - ]); - }, - { - message: "Client error occurred: Request failed with status code '409'.", - }, - ); - }); - - suite('when using the isSubjectPristine precondition', (): void => { - test('writes the events if the subject is pristine.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents( - [ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - ), - ], - [isSubjectPristine({ subject: '/users/registered' })], - ); - }); - - test('throws an error if the subject is not pristine.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents( - [ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - ), - ], - [isSubjectPristine({ subject: '/users/registered' })], - ); - - await assert.rejects( - async () => { - await client.writeEvents( - [ - source.newEvent( - '/users/registered', - events.registered.johnDoe.type, - events.registered.johnDoe.data, - ), - ], - [isSubjectPristine({ subject: '/users/registered' })], - ); - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Request failed with status code '409'.", - ); - return true; - }, - ); - }); - }); - - suite('when using the isSubjectOnEventId precondition', (): void => { - test('writes the events if the last event of the subject has the given event ID.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - ), - source.newEvent( - '/users/registered', - events.registered.johnDoe.type, - events.registered.johnDoe.data, - ), - ]); - - const readEventsResult = database.withAuthorization.client.readEvents( - new AbortController(), - '/users/registered', - { recursive: false }, - ); - - const readItems: StoreItem[] = []; - for await (const item of readEventsResult) { - readItems.push(item); - } - const lastEventId = readItems[readItems.length - 1].event.id; - - // Should not throw. - await client.writeEvents( - [ - source.newEvent( - '/users/registered', - events.registered.apfelFred.type, - events.registered.apfelFred.data, - ), - ], - [isSubjectOnEventId({ subject: '/users/registered', eventId: lastEventId })], - ); - }); - - test('throws an error if the last event of the subject does not have the given event ID.', async (): Promise => { - const client = database.withAuthorization.client; - - // Should not throw. - await client.writeEvents([ - source.newEvent( - '/users/registered', - events.registered.janeDoe.type, - events.registered.janeDoe.data, - ), - source.newEvent( - '/users/registered', - events.registered.johnDoe.type, - events.registered.johnDoe.data, - ), - ]); - - const lastEventId = '1337'; - - await assert.rejects( - async () => { - await client.writeEvents( - [ - source.newEvent( - '/users/registered', - events.registered.apfelFred.type, - events.registered.apfelFred.data, - ), - ], - [isSubjectOnEventId({ subject: '/users/registered', eventId: lastEventId })], - ); - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Request failed with status code '409'.", - ); - return true; - }, - ); - }); - }); - - suite('using mocked HTTP server', (): void => { - let stopServer: () => Promise; - - afterEach(async () => { - await stopServer(); - }); - - const events = [source.newEvent('/', 'com.foobar.baz', {})]; - - test('throws a server error if the server responds with http 5xx on every try.', async () => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/write-events', (_req, res) => { - res.status(StatusCodes.BAD_GATEWAY); - res.send(ReasonPhrases.BAD_GATEWAY); - }); - })); - - await assert.rejects( - async () => { - await client.writeEvents(events); - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - "Server error occurred: Request failed with status code '502'.", - ); - return true; - }, - ); - }); - - test("throws an error if the server's protocol version does not match.", async () => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/write-events', (_req, res) => { - res.setHeader('X-EventSourcingDB-Protocol-Version', '0.0.0'); - res.status(StatusCodes.UNPROCESSABLE_ENTITY); - res.send(ReasonPhrases.UNPROCESSABLE_ENTITY); - }); - })); - - await assert.rejects( - async () => { - await client.writeEvents(events); - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Protocol version mismatch, server '0.0.0', client '1.0.0.'", - ); - return true; - }, - ); - }); - - test('throws a client error if the server returns a 4xx status code.', async () => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/write-events', (_req, res) => { - res.status(StatusCodes.IM_A_TEAPOT); - res.send(ReasonPhrases.IM_A_TEAPOT); - }); - })); - - await assert.rejects( - async () => { - await client.writeEvents(events); - }, - error => { - assert.ok(error instanceof ClientError); - assert.equal( - error.message, - "Client error occurred: Request failed with status code '418'.", - ); - return true; - }, - ); - }); - - test('returns a server error if the server returns a non 200, 5xx or 4xx status code.', async () => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/write-events', (_req, res) => { - res.status(StatusCodes.ACCEPTED); - res.send(ReasonPhrases.ACCEPTED); - }); - })); - - await assert.rejects( - async () => { - await client.writeEvents(events); - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - 'Server error occurred: Unexpected response status: 202 Accepted.', - ); - return true; - }, - ); - }); - - test("throws a server error if the server's response can't be parsed.", async () => { - let client: Client; - ({ client, stopServer } = await startLocalHttpServer(app => { - app.post('/api/write-events', (_req, res) => { - res.send('utter garbage'); - }); - })); - - await assert.rejects( - async () => { - await client.writeEvents(events); - }, - error => { - assert.ok(error instanceof ServerError); - assert.equal( - error.message, - "Server error occurred: Failed to parse response 'utter garbage' to array.", - ); - return true; - }, - ); - }); - }); -}); diff --git a/test/observeEvents.test.ts b/test/observeEvents.test.ts new file mode 100644 index 0000000..11ed60f --- /dev/null +++ b/test/observeEvents.test.ts @@ -0,0 +1,240 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import type { Event } from '../src/Event.js'; +import type { EventCandidate } from '../src/EventCandidate.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('observeEvents', { timeout: 5_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('observes no events if the database is empty.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const controller = new AbortController(); + + setTimeout(() => { + controller.abort(); + }, 100); + + let didObserveEvents = false; + for await (const _event of client.observeEvents( + '/', + { + recursive: true, + }, + controller.signal, + )) { + didObserveEvents = true; + } + + assert.equal(didObserveEvents, false); + }); + + test('observes all events from the given subject.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const controller = new AbortController(); + + setTimeout(() => { + controller.abort(); + }, 100); + + const eventsRead: Event[] = []; + for await (const event of client.observeEvents( + '/test', + { + recursive: false, + }, + controller.signal, + )) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 2); + }); + + test('observes recursively.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const controller = new AbortController(); + + setTimeout(() => { + controller.abort(); + }, 100); + + const eventsRead: Event[] = []; + for await (const event of client.observeEvents( + '/', + { + recursive: true, + }, + controller.signal, + )) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 2); + }); + + test('observes with lower bound.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const controller = new AbortController(); + + setTimeout(() => { + controller.abort(); + }, 100); + + const eventsRead: Event[] = []; + for await (const event of client.observeEvents( + '/test', + { + recursive: false, + lowerBound: { id: '1', type: 'inclusive' }, + }, + controller.signal, + )) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 1); + assert.equal(eventsRead[0].data.value, 42); + }); + + test('observes from latest event.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test.foo', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test.bar', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const controller = new AbortController(); + + setTimeout(() => { + controller.abort(); + }, 100); + + const eventsRead: Event[] = []; + for await (const event of client.observeEvents( + '/test', + { + recursive: false, + fromLatestEvent: { + subject: '/test', + type: 'io.eventsourcingdb.test.bar', + ifEventIsMissing: 'read-everything', + }, + }, + controller.signal, + )) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 1); + assert.equal(eventsRead[0].data.value, 42); + }); +}); diff --git a/test/ping.test.ts b/test/ping.test.ts new file mode 100644 index 0000000..827cd11 --- /dev/null +++ b/test/ping.test.ts @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('ping', { timeout: 5_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('does not throw an error if the server is reachable.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + // Should not throw. + await client.ping(); + }); + + test('throws an error if the server is not reachable.', async (): Promise => { + const client = new Client( + new URL(`http://non-existent-host:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + await assert.rejects( + async () => { + await client.ping(); + }, + error => { + assert.ok(error instanceof Error); + assert.equal(error.message, 'fetch failed'); + return true; + }, + ); + }); +}); diff --git a/test/readEventTypes.test.ts b/test/readEventTypes.test.ts new file mode 100644 index 0000000..4c4c5a3 --- /dev/null +++ b/test/readEventTypes.test.ts @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import type { EventCandidate } from '../src/EventCandidate.js'; +import type { EventType } from '../src/EventType.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('readEventTypes', { timeout: 20_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('reads no event types if the database is empty.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + let didReadEventTypes = false; + for await (const _event of client.readEventTypes()) { + didReadEventTypes = true; + } + + assert.equal(didReadEventTypes, false); + }); + + test('reads all event types.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test/1', + type: 'io.eventsourcingdb.test.foo', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test/2', + type: 'io.eventsourcingdb.test.bar', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventTypesRead: EventType[] = []; + for await (const eventType of client.readEventTypes()) { + eventTypesRead.push(eventType); + } + + assert.deepEqual(eventTypesRead, [ + { + eventType: 'io.eventsourcingdb.test.bar', + isPhantom: false, + }, + { + eventType: 'io.eventsourcingdb.test.foo', + isPhantom: false, + }, + ]); + }); + + test('supports reading event schemas.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const eventType = 'io.eventsourcingdb.test'; + const schema = { + type: 'object', + properties: { + value: { + type: 'number', + }, + }, + required: ['value'], + additionalProperties: false, + }; + + await client.registerEventSchema(eventType, schema); + + const eventTypesRead: EventType[] = []; + for await (const eventType of client.readEventTypes()) { + eventTypesRead.push(eventType); + } + + assert.deepEqual(eventTypesRead, [ + { + eventType: 'io.eventsourcingdb.test', + isPhantom: true, + schema, + }, + ]); + }); +}); diff --git a/test/readEvents.test.ts b/test/readEvents.test.ts new file mode 100644 index 0000000..7d64641 --- /dev/null +++ b/test/readEvents.test.ts @@ -0,0 +1,306 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import type { Event } from '../src/Event.js'; +import type { EventCandidate } from '../src/EventCandidate.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('readEvents', { timeout: 5_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('reads no events if the database is empty.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + let didReadEvents = false; + for await (const _event of client.readEvents('/', { + recursive: true, + })) { + didReadEvents = true; + } + + assert.equal(didReadEvents, false); + }); + + test('reads all events from the given subject.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventsRead: Event[] = []; + for await (const event of client.readEvents('/test', { + recursive: false, + })) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 2); + }); + + test('reads recursively.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventsRead: Event[] = []; + for await (const event of client.readEvents('/', { + recursive: true, + })) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 2); + }); + + test('reads chronologically.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventsRead: Event[] = []; + for await (const event of client.readEvents('/test', { + recursive: false, + order: 'chronological', + })) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 2); + assert.equal(eventsRead[0].data.value, 23); + assert.equal(eventsRead[1].data.value, 42); + }); + + test('reads anti-chronologically.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventsRead: Event[] = []; + for await (const event of client.readEvents('/test', { + recursive: false, + order: 'antichronological', + })) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 2); + assert.equal(eventsRead[0].data.value, 42); + assert.equal(eventsRead[1].data.value, 23); + }); + + test('reads with lower bound.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventsRead: Event[] = []; + for await (const event of client.readEvents('/test', { + recursive: false, + lowerBound: { id: '1', type: 'inclusive' }, + })) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 1); + assert.equal(eventsRead[0].data.value, 42); + }); + + test('reads with upper bound.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventsRead: Event[] = []; + for await (const event of client.readEvents('/test', { + recursive: false, + upperBound: { id: '0', type: 'inclusive' }, + })) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 1); + assert.equal(eventsRead[0].data.value, 23); + }); + + test('reads from latest event.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test.foo', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test.bar', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const eventsRead: Event[] = []; + for await (const event of client.readEvents('/test', { + recursive: false, + fromLatestEvent: { + subject: '/test', + type: 'io.eventsourcingdb.test.bar', + ifEventIsMissing: 'read-everything', + }, + })) { + eventsRead.push(event); + } + + assert.equal(eventsRead.length, 1); + assert.equal(eventsRead[0].data.value, 42); + }); +}); diff --git a/test/readSubjects.test.ts b/test/readSubjects.test.ts new file mode 100644 index 0000000..999b0f9 --- /dev/null +++ b/test/readSubjects.test.ts @@ -0,0 +1,110 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import type { EventCandidate } from '../src/EventCandidate.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('readSubjects', { timeout: 5_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('reads no subjects if the database is empty.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + let didReadSubjects = false; + for await (const _event of client.readSubjects('/')) { + didReadSubjects = true; + } + + assert.equal(didReadSubjects, false); + }); + + test('reads all subjects.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test/1', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test/2', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const subjectsRead: string[] = []; + for await (const subject of client.readSubjects('/')) { + subjectsRead.push(subject); + } + + assert.equal(subjectsRead.length, 4); + assert.equal(subjectsRead[0], '/'); + assert.equal(subjectsRead[1], '/test'); + assert.equal(subjectsRead[2], '/test/1'); + assert.equal(subjectsRead[3], '/test/2'); + }); + + test('reads all subjects from the base subject.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test/1', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test/2', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await client.writeEvents([firstEvent, secondEvent]); + + const subjectsRead: string[] = []; + for await (const subject of client.readSubjects('/test')) { + subjectsRead.push(subject); + } + + assert.equal(subjectsRead.length, 3); + assert.equal(subjectsRead[0], '/test'); + assert.equal(subjectsRead[1], '/test/1'); + assert.equal(subjectsRead[2], '/test/2'); + }); +}); diff --git a/test/registerEventSchema.test.ts b/test/registerEventSchema.test.ts new file mode 100644 index 0000000..314ba44 --- /dev/null +++ b/test/registerEventSchema.test.ts @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('registerEventSchema', { timeout: 5_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('registers an event schema.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const eventType = 'io.eventsourcingdb.test'; + const schema = { + type: 'object', + properties: { + value: { + type: 'number', + }, + }, + required: ['value'], + additionalProperties: false, + }; + + // Should not throw. + await client.registerEventSchema(eventType, schema); + }); + + test('throws an error if an event schema is already registered.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const eventType = 'io.eventsourcingdb.test'; + const schema = { + type: 'object', + properties: { + value: { + type: 'number', + }, + }, + required: ['value'], + additionalProperties: false, + }; + + await client.registerEventSchema(eventType, schema); + + await assert.rejects( + async () => { + await client.registerEventSchema(eventType, schema); + }, + error => { + assert.ok(error instanceof Error); + assert.equal( + error.message, + `Failed to register event schema, got HTTP status code '409', expected '200'.`, + ); + return true; + }, + ); + }); +}); diff --git a/test/shared/ContainerizedTestingDatabase.ts b/test/shared/ContainerizedTestingDatabase.ts deleted file mode 100644 index 62a02d2..0000000 --- a/test/shared/ContainerizedTestingDatabase.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { setTimeout } from 'node:timers/promises'; -import type { ClientOptions } from '../../src/ClientOptions.js'; -import { Client } from '../../src/index.js'; -import type { Container } from './docker/Container.js'; -import type { Image } from './docker/Image.js'; - -class ContainerizedTestingDatabase { - private readonly command: string; - - private container: Container; - - public readonly client: Client; - - private readonly image: Image; - - private readonly options: ClientOptions; - - private constructor( - image: Image, - command: string, - options: ClientOptions, - client: Client, - container: Container, - ) { - this.command = command; - this.image = image; - this.options = options; - this.client = client; - this.container = container; - } - - public static async create( - image: Image, - command: string, - options: ClientOptions, - ): Promise { - const { client, container } = await ContainerizedTestingDatabase.start(image, command, options); - - return new ContainerizedTestingDatabase(image, command, options, client, container); - } - - private static async start(image: Image, command: string, options: ClientOptions) { - const container = image.run(command, true, true); - const exposedPort = container.getExposedPort(3_000); - const baseUrl = `http://127.0.0.1:${exposedPort}`; - const client = new Client(baseUrl, options); - - for (let i = 0; i < 10; i++) { - try { - await client.ping(); - break; - } catch { - // We intentionally ignore server error exceptions since this is expected to fail the first few times. - } - await setTimeout(1_000); - } - - return { - container, - client, - }; - } - - public stop(): void { - this.container.kill(); - } -} - -export { ContainerizedTestingDatabase }; diff --git a/test/shared/Database.ts b/test/shared/Database.ts deleted file mode 100644 index 032ca3a..0000000 --- a/test/shared/Database.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ContainerizedTestingDatabase } from './ContainerizedTestingDatabase.js'; -import type { TestingDatabase } from './TestingDatabase.js'; - -interface Database { - withAuthorization: ContainerizedTestingDatabase; - withInvalidUrl: TestingDatabase; -} - -export type { Database }; diff --git a/test/shared/TestingDatabase.ts b/test/shared/TestingDatabase.ts deleted file mode 100644 index fad50a9..0000000 --- a/test/shared/TestingDatabase.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Client } from '../../src/index.js'; - -class TestingDatabase { - public readonly client: Client; - - public constructor(client: Client) { - this.client = client; - } -} - -export { TestingDatabase }; diff --git a/test/shared/abortController/newAbortControllerWithDeadline.ts b/test/shared/abortController/newAbortControllerWithDeadline.ts deleted file mode 100644 index 5b01d60..0000000 --- a/test/shared/abortController/newAbortControllerWithDeadline.ts +++ /dev/null @@ -1,15 +0,0 @@ -const newAbortControllerWithDeadline = (deadlineMilliseconds: number): AbortController => { - const abortController = new AbortController(); - - setTimeout(() => { - if (abortController.signal.aborted) { - return; - } - - abortController.abort(); - }, deadlineMilliseconds); - - return abortController; -}; - -export { newAbortControllerWithDeadline }; diff --git a/test/shared/buildDatabase.ts b/test/shared/buildDatabase.ts deleted file mode 100644 index 208e0a2..0000000 --- a/test/shared/buildDatabase.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Image } from './docker/Image.js'; - -const buildDatabase = (dockerfileDirectory: string): void => { - const image = new Image('eventsourcingdb', 'latest'); - image.build(dockerfileDirectory); -}; - -export { buildDatabase }; diff --git a/test/shared/docker/Container.ts b/test/shared/docker/Container.ts deleted file mode 100644 index 3d471e1..0000000 --- a/test/shared/docker/Container.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { exec } from 'shelljs'; - -class Container { - private readonly id: string; - - public constructor(id: string) { - this.id = id; - } - - public kill(): void { - const { code, stderr } = exec(`docker kill ${this.id}`, { silent: true }); - - if (code !== 0) { - throw new Error(`Kill failed with output: ${stderr}`); - } - } - - public getExposedPort(internalPort: number): number { - const { code, stderr, stdout } = exec( - `docker inspect --format='{{(index (index .NetworkSettings.Ports "${internalPort}/tcp") 0).HostPort}}' ${this.id}`, - { silent: true }, - ); - - if (code !== 0) { - throw new Error(`Inspect failed with output: ${stderr}`); - } - - const exposedPortAsString = stdout.replaceAll(/\s|'/gu, ''); - const exposedPort = Number.parseInt(exposedPortAsString, 10); - - if (Number.isNaN(exposedPort)) { - throw new Error(`Could not parse port from: ${exposedPortAsString}`); - } - - return exposedPort; - } -} - -export { Container }; diff --git a/test/shared/docker/Image.ts b/test/shared/docker/Image.ts deleted file mode 100644 index 44497ef..0000000 --- a/test/shared/docker/Image.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { exec } from 'shelljs'; -import { Container } from './Container.js'; - -class Image { - private readonly name: string; - - private readonly tag: string; - - public constructor(name: string, tag: string) { - this.name = name; - this.tag = tag; - } - - public run(command: string, isDetached: boolean, doExposePorts: boolean): Container { - let dockerCommand = 'docker run --rm'; - - if (isDetached) { - dockerCommand += ' -d'; - } - if (doExposePorts) { - dockerCommand += ' -P'; - } - - dockerCommand += ` ${this.getFullName()} ${command}`; - - const { code, stdout, stderr } = exec(dockerCommand, { silent: true }); - - if (code !== 0) { - throw new Error(`Run failed with output: ${stderr}`); - } - - const containerId = stdout.trim(); - - return new Container(containerId); - } - - public build(directory: string): void { - const { code, stderr } = exec(`docker build -t ${this.getFullName()} ${directory}`, { - silent: true, - }); - - if (code !== 0) { - throw new Error(`Build failed with output: ${stderr}`); - } - } - - private getFullName(): string { - return `${this.name}:${this.tag}`; - } -} - -export { Image }; diff --git a/test/shared/docker/eventsourcingdb/Dockerfile b/test/shared/docker/eventsourcingdb/Dockerfile deleted file mode 100644 index 388a0b2..0000000 --- a/test/shared/docker/eventsourcingdb/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -FROM thenativeweb/eventsourcingdb:0.81.2 diff --git a/test/shared/events/events.ts b/test/shared/events/events.ts deleted file mode 100644 index 89ea95d..0000000 --- a/test/shared/events/events.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { prefixEventType } from './type.js'; - -const registeredEventType = prefixEventType('registered'); -const loggedInEventType = prefixEventType('loggedIn'); - -interface PartialEvent { - type: string; - data: Record; - traceState?: string; - traceParent?: string; -} - -const events: Record> = { - registered: { - janeDoe: { - type: registeredEventType, - data: { name: 'Jane Doe' }, - traceParent: '00-10000000000000000000000000000000-1000000000000000-00', - }, - johnDoe: { - type: registeredEventType, - data: { name: 'John Doe' }, - traceParent: '00-20000000000000000000000000000000-2000000000000000-00', - }, - apfelFred: { - type: registeredEventType, - data: { name: 'Apfel Fred' }, - traceParent: '00-30000000000000000000000000000000-3000000000000000-00', - }, - }, - loggedIn: { - janeDoe: { - type: loggedInEventType, - data: { name: 'Jane Doe' }, - traceParent: '00-40000000000000000000000000000000-4000000000000000-00', - }, - johnDoe: { - type: loggedInEventType, - data: { name: 'John Doe' }, - traceParent: '00-50000000000000000000000000000000-5000000000000000-00', - }, - apfelFred: { - type: loggedInEventType, - data: { name: 'Apfel Fred' }, - traceParent: '00-60000000000000000000000000000000-6000000000000000-00', - }, - }, -} as const; - -export { events }; diff --git a/test/shared/events/source.ts b/test/shared/events/source.ts deleted file mode 100644 index 72dfce0..0000000 --- a/test/shared/events/source.ts +++ /dev/null @@ -1,3 +0,0 @@ -const testSource = 'tag:thenativeweb.io,2023:eventsourcingdb:test'; - -export { testSource }; diff --git a/test/shared/events/type.ts b/test/shared/events/type.ts deleted file mode 100644 index 1cbeda3..0000000 --- a/test/shared/events/type.ts +++ /dev/null @@ -1,5 +0,0 @@ -const prefixEventType = (suffix: string): string => { - return `io.thenativeweb.eventsourcingdb.test.${suffix}`; -}; - -export { prefixEventType }; diff --git a/test/shared/startDatabase.ts b/test/shared/startDatabase.ts deleted file mode 100644 index 4e2d449..0000000 --- a/test/shared/startDatabase.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import { Client } from '../../src/index.js'; -import { ContainerizedTestingDatabase } from './ContainerizedTestingDatabase.js'; -import type { Database } from './Database.js'; -import { TestingDatabase } from './TestingDatabase.js'; -import { Image } from './docker/Image.js'; - -const startDatabase = async (): Promise => { - const image = new Image('eventsourcingdb', 'latest'); - - const accessToken = randomUUID(); - const withAuthorization = await ContainerizedTestingDatabase.create( - image, - `run --ui --access-token ${accessToken} --store-temporary`, - { accessToken }, - ); - const withInvalidUrl = new TestingDatabase( - new Client('http://localhost.invalid', { accessToken }), - ); - - return { - withAuthorization, - withInvalidUrl, - }; -}; - -export { startDatabase }; diff --git a/test/shared/startLocalHttpServer.ts b/test/shared/startLocalHttpServer.ts deleted file mode 100644 index 64d01d1..0000000 --- a/test/shared/startLocalHttpServer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type http from 'node:http'; -import express from 'express'; -import { Client } from '../../src/index.js'; - -const startLocalHttpServer = async ( - attachHandlers: (app: express.Express) => void, -): Promise<{ client: Client; stopServer: () => Promise }> => { - const app = express(); - - attachHandlers(app); - - const server = await new Promise(resolve => { - const _server = app.listen(0, () => { - resolve(_server); - }); - }); - - if (server === undefined) { - throw new Error('Failed to start server.'); - } - const address = server.address(); - - if (typeof address === 'string' || address === null) { - throw new Error('Failed to start server'); - } - - const client = new Client(`http://localhost:${address.port}`, { - accessToken: 'irrelevant', - }); - const stopServer = async () => { - await new Promise(resolve => { - server.close(() => { - resolve(); - }); - }); - }; - - return { client, stopServer }; -}; - -export { startLocalHttpServer }; diff --git a/test/shared/stopDatabase.ts b/test/shared/stopDatabase.ts deleted file mode 100644 index 19e4304..0000000 --- a/test/shared/stopDatabase.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Database } from './Database.js'; - -const stopDatabase = (database: Database): void => { - database.withAuthorization.stop(); -}; - -export { stopDatabase }; diff --git a/test/unit/event/EventContextTests.ts b/test/unit/event/EventContextTests.ts deleted file mode 100644 index 689716b..0000000 --- a/test/unit/event/EventContextTests.ts +++ /dev/null @@ -1,224 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { EventContext } from '../../../src/event/EventContext.js'; -import { ValidationError } from '../../../src/util/error/ValidationError.js'; -import { events } from '../../shared/events/events.js'; -import { testSource } from '../../shared/events/source.js'; - -suite('EventContext', () => { - suite('parse', () => { - test('returns an EventContext for a valid object.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - // Should not throw. - EventContext.parse(toParse); - }); - - test('throws an error for invalid source.', () => { - const toParse = { - source: 42, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal(error.message, "Failed to parse source '42' to string."); - return true; - }, - ); - }); - - test('throws an error for invalid subject.', () => { - const toParse = { - source: testSource, - subject: 'this/is/invalid', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - error.message, - "Failed to validate subject: 'this/is/invalid' must be an absolute, slash-separated path.", - ); - return true; - }, - ); - }); - - test('throws an error for invalid type.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: 'a.this.is.invalid', - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - error.message, - "Failed to validate type: 'a.this.is.invalid' must be a reverse domain name.", - ); - return true; - }, - ); - }); - - test('throws an error for invalid specversion.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: 1.0, - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal(error.message, "Failed to parse specVersion '1' to string."); - return true; - }, - ); - }); - - test('throws an error for invalid id.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: { id: 'id' }, - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal(error.message, "Failed to parse id '[object Object]' to string."); - return true; - }, - ); - }); - - test('throws an error for invalid time.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: 'not a date', - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal(error.message, "Failed to parse time 'not a date' to Date."); - return true; - }, - ); - }); - - test('throws an error for invalid dataContentType.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: undefined, - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal(error.message, "Failed to parse dataContentType 'undefined' to string."); - return true; - }, - ); - }); - - test('throws an error for invalid dataContentType.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: null, - }; - - assert.throws( - () => { - EventContext.parse(toParse); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal(error.message, "Failed to parse predecessorHash 'null' to string."); - return true; - }, - ); - }); - }); -}); diff --git a/test/unit/event/EventTests.ts b/test/unit/event/EventTests.ts deleted file mode 100644 index 20f807d..0000000 --- a/test/unit/event/EventTests.ts +++ /dev/null @@ -1,71 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { Event } from '../../../src/index.js'; -import { events } from '../../shared/events/events.js'; -import { testSource } from '../../shared/events/source.js'; - -suite('Event', () => { - suite('parse', () => { - test('returns an Event for a valid object.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - data: { someKey: 'some-data' }, - }; - - // Should not throw. - Event.parse(toParse); - }); - - test('throws an error for missing data.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - }; - - assert.throws( - () => { - Event.parse(toParse); - }, - { - message: "Failed to parse data 'undefined' to object.", - }, - ); - }); - - test('throws an error for invalid data.', () => { - const toParse = { - source: testSource, - subject: '/users/john-doe/loggedIn', - type: events.loggedIn.johnDoe.type, - specversion: '1.0.0', - id: 'some-id', - time: new Date().toISOString(), - datacontenttype: 'some-data-content-type', - predecessorhash: 'some-predecessor-hash', - data: 42, - }; - - assert.throws( - () => { - Event.parse(toParse); - }, - { - message: "Failed to parse data '42' to object.", - }, - ); - }); - }); -}); diff --git a/test/unit/event/validateSubjectTests.ts b/test/unit/event/validateSubjectTests.ts deleted file mode 100644 index ca6f888..0000000 --- a/test/unit/event/validateSubjectTests.ts +++ /dev/null @@ -1,75 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { validateSubject } from '../../../src/event/validateSubject.js'; -import { ValidationError } from '../../../src/util/error/ValidationError.js'; - -suite('validateSubject', () => { - test('returns without throwing on a valid subject.', () => { - // Should not throw. - validateSubject('/this/is/valid'); - }); - - test('contains the invalid subject in the error message in case of throwing.', () => { - assert.throws( - () => { - validateSubject('invalidExampleSubject'); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.ok(error.message.includes('invalidExampleSubject')); - return true; - }, - ); - }); - - test('throws if the subject is not an absolute slash separated path.', () => { - const subject = 'invalidExampleSubject'; - assert.throws( - () => { - validateSubject(subject); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate subject: '${subject}' must be an absolute, slash-separated path.`, - error.message, - ); - return true; - }, - ); - }); - - test('throws if the subject is a relative path.', () => { - const subject = 'this/is/invalid'; - assert.throws( - () => { - validateSubject(subject); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate subject: '${subject}' must be an absolute, slash-separated path.`, - error.message, - ); - return true; - }, - ); - }); - - test('throws if the subject has invalid characters.', () => { - const subject = '/user/günter/registered'; - assert.throws( - () => { - validateSubject(subject); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate subject: '${subject}' must be an absolute, slash-separated path.`, - error.message, - ); - return true; - }, - ); - }); -}); diff --git a/test/unit/event/validateTypeTests.ts b/test/unit/event/validateTypeTests.ts deleted file mode 100644 index af2a44b..0000000 --- a/test/unit/event/validateTypeTests.ts +++ /dev/null @@ -1,113 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { validateType } from '../../../src/event/validateType.js'; -import { ValidationError } from '../../../src/util/error/ValidationError.js'; - -suite('validateType', () => { - test('returns without throwing on a valid type.', () => { - // Should not throw. - validateType('com.example.exampleType'); - }); - - test('contains the invalid type in the error message in case of throwing an error.', () => { - const eventType = 'invalidExampleType'; - assert.throws( - () => { - validateType(eventType); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate type: '${eventType}' must be a reverse domain name.`, - error.message, - ); - return true; - }, - ); - }); - - test('throws an error if the type is not a reverse domain name.', () => { - const eventType = 'invalidExampleType'; - assert.throws( - () => { - validateType(eventType); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate type: '${eventType}' must be a reverse domain name.`, - error.message, - ); - return true; - }, - ); - }); - - test("throws an error if the separator is not not a '.'.", () => { - const eventType = 'com:example:exampleType'; - assert.throws( - () => { - validateType(eventType); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate type: '${eventType}' must be a reverse domain name.`, - error.message, - ); - return true; - }, - ); - }); - - test('throws an error if the reverse domain has less than 3 segments.', () => { - const eventType = 'com.example'; - assert.throws( - () => { - validateType(eventType); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate type: '${eventType}' must be a reverse domain name.`, - error.message, - ); - return true; - }, - ); - }); - - test('throws an error if the type has invalid characters.', () => { - const eventType = 'com.example.apfel-günter.registered'; - assert.throws( - () => { - validateType(eventType); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate type: '${eventType}' must be a reverse domain name.`, - error.message, - ); - return true; - }, - ); - }); - - test('throws an error if the tld of the reverse domain has less than 1 character.', () => { - const eventType = 'a.example.exampleType'; - assert.throws( - () => { - validateType(eventType); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - `Failed to validate type: '${eventType}' must be a reverse domain name.`, - error.message, - ); - return true; - }, - ); - }); -}); diff --git a/test/unit/handlers/isHeartbeatTests.ts b/test/unit/handlers/isHeartbeatTests.ts deleted file mode 100644 index 6252b2b..0000000 --- a/test/unit/handlers/isHeartbeatTests.ts +++ /dev/null @@ -1,31 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { isHeartbeat } from '../../../src/handlers/isHeartbeat.js'; - -suite('isHeartbeat', () => { - test('returns true for a heartbeat object.', () => { - assert.ok( - isHeartbeat({ - type: 'heartbeat', - }), - ); - }); - - test('ignores additional attributes.', () => { - assert.ok( - isHeartbeat({ - type: 'heartbeat', - additional: 'attribute', - }), - ); - }); - - test('returns false for a non heartbeat object.', () => { - assert.equal( - isHeartbeat({ - type: 'not-a-heartbeat', - }), - false, - ); - }); -}); diff --git a/test/unit/handlers/isItemTests.ts b/test/unit/handlers/isItemTests.ts deleted file mode 100644 index 11dc863..0000000 --- a/test/unit/handlers/isItemTests.ts +++ /dev/null @@ -1,64 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { isItem } from '../../../src/handlers/isItem.js'; - -suite('isItem', () => { - test('returns true for a item object.', () => { - assert.ok( - isItem({ - type: 'item', - payload: { - event: {}, - hash: 'some-hash', - }, - }), - ); - }); - - test('ignores additional attributes.', () => { - assert.ok( - isItem({ - type: 'item', - payload: { - event: {}, - hash: 'some-hash', - }, - additional: 'attribute', - }), - ); - }); - - test('returns false for a missing payload.', () => { - assert.equal( - isItem({ - type: 'item', - }), - false, - ); - }); - - test('returns false for a invalid payload.', () => { - assert.equal( - isItem({ - type: 'item', - payload: { - event: {}, - }, - }), - false, - ); - }); - - test('returns false for a non item object.', () => { - assert.equal( - isItem({ - type: 'not-an-item', - payload: { - event: {}, - hash: 'some-hash', - }, - }), - false, - ); - }); -}); diff --git a/test/unit/handlers/isStreamErrorTests.ts b/test/unit/handlers/isStreamErrorTests.ts deleted file mode 100644 index c2b85dd..0000000 --- a/test/unit/handlers/isStreamErrorTests.ts +++ /dev/null @@ -1,59 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { isStreamError } from '../../../src/handlers/isStreamError.js'; - -suite('isStreamError', () => { - test('returns true for a error object.', () => { - assert.ok( - isStreamError({ - type: 'error', - payload: { - error: 'some-error', - }, - }), - ); - }); - - test('ignores additional attributes.', () => { - assert.ok( - isStreamError({ - type: 'error', - payload: { - error: 'some-error', - }, - additional: 'attribute', - }), - ); - }); - - test('returns false for a missing payload', () => { - assert.equal( - isStreamError({ - type: 'error', - }), - false, - ); - }); - - test('returns false for a invalid payload', () => { - assert.equal( - isStreamError({ - type: 'error', - payload: {}, - }), - false, - ); - }); - - test('returns false for a non error object', () => { - assert.equal( - isStreamError({ - type: 'not-an-error', - payload: { - error: 'some-error', - }, - }), - false, - ); - }); -}); diff --git a/test/unit/handlers/observeEvents/ObserveEventsOptions.ts b/test/unit/handlers/observeEvents/ObserveEventsOptions.ts deleted file mode 100644 index 8318373..0000000 --- a/test/unit/handlers/observeEvents/ObserveEventsOptions.ts +++ /dev/null @@ -1,37 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { validateObserveEventsOptions } from '../../../../src/handlers/observeEvents/ObserveEventsOptions.js'; -import { ValidationError } from '../../../../src/util/error/ValidationError.js'; - -suite('validateObserveEventOptions', () => { - test('returns for a valid options object.', () => { - // Should not throw. - validateObserveEventsOptions({ - recursive: false, - }); - }); - - test('throws an error if the both lowerBoundId and fromLatestEvent were given.', () => { - assert.throws( - () => { - validateObserveEventsOptions({ - recursive: false, - lowerBoundId: '1', - fromLatestEvent: { - subject: 'some-subject', - type: 'some-type', - ifEventIsMissing: 'wait-for-event', - }, - }); - }, - error => { - assert.ok(error instanceof ValidationError); - assert.equal( - 'ObserveEventsOptions are invalid: lowerBoundId and fromLatestEvent are mutually exclusive.', - error.message, - ); - return true; - }, - ); - }); -}); diff --git a/test/unit/utils/isObjectTests.ts b/test/unit/utils/isObjectTests.ts deleted file mode 100644 index a7514b7..0000000 --- a/test/unit/utils/isObjectTests.ts +++ /dev/null @@ -1,19 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { isObject } from '../../../src/util/isObject.js'; - -suite('isObject', (): void => { - test('returns false if the given value is not an object.', (): void => { - assert.equal(isObject(1), false); - assert.equal(isObject(''), false); - assert.equal(isObject([]), false); - assert.equal(isObject(null), false); - assert.equal(isObject(undefined), false); - assert.equal(isObject(true), false); - }); - - test('returns true if the given value is an object.', (): void => { - assert.ok(isObject({})); - assert.ok(isObject(new Map())); - }); -}); diff --git a/test/unit/utils/ndjson/LinesDecoderTests.ts b/test/unit/utils/ndjson/LinesDecoderTests.ts deleted file mode 100644 index 99cef5e..0000000 --- a/test/unit/utils/ndjson/LinesDecoderTests.ts +++ /dev/null @@ -1,25 +0,0 @@ -import assert from 'node:assert/strict'; -import { suite, test } from 'node:test'; -import { LinesDecoder } from '../../../../src/util/ndjson/LinesDecoder.js'; - -suite('LinesDecoder', (): void => { - test('returns all completed lines on write().', (): void => { - const decoder = new LinesDecoder(); - - const actualLines = decoder.write(Buffer.from('hello\nworld\nfoobar')); - - assert.deepEqual(actualLines, ['hello', 'world']); - }); - - test('buffers incomplete lines and returns them on write() as soon as they are completed.', (): void => { - const decoder = new LinesDecoder(); - - let actualLines = decoder.write(Buffer.from('incomplete')); - - assert.deepEqual(actualLines, []); - - actualLines = decoder.write(Buffer.from(' line\n')); - - assert.deepEqual(actualLines, ['incomplete line']); - }); -}); diff --git a/test/unit/utils/ndjson/readNdJsonStream.ts b/test/unit/utils/ndjson/readNdJsonStream.ts deleted file mode 100644 index c493be3..0000000 --- a/test/unit/utils/ndjson/readNdJsonStream.ts +++ /dev/null @@ -1,20 +0,0 @@ -import assert from 'node:assert/strict'; -import { Readable } from 'node:stream'; -import { suite, test } from 'node:test'; -import type { UnknownObject } from '../../../../src/util/UnknownObject.js'; -import { readNdJsonStream } from '../../../../src/util/ndjson/readNdJsonStream.js'; - -suite('readNdJsonStream', (): void => { - test('returns an async generator that yields parsed json objects.', async (): Promise => { - const stream = Readable.from( - Buffer.from('{"foo":"bar"}\n{"bar":"baz"}\n{"incomplete', 'utf-8'), - ); - - const actualMessages: UnknownObject[] = []; - for await (const message of readNdJsonStream(stream)) { - actualMessages.push(message); - } - - assert.deepEqual(actualMessages, [{ foo: 'bar' }, { bar: 'baz' }]); - }); -}); diff --git a/test/verifyApiToken.test.ts b/test/verifyApiToken.test.ts new file mode 100644 index 0000000..e7da9b8 --- /dev/null +++ b/test/verifyApiToken.test.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('verifyApiToken', { timeout: 5_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('does not throw an error if the token is valid.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + // Should not throw. + await client.verifyApiToken(); + }); + + test('throws an error if the token is invalid.', async (): Promise => { + const invalidToken = `${eventSourcingDb.apiToken}-invalid`; + const client = new Client(new URL(`http://localhost:${eventSourcingDb.port}/`), invalidToken); + + await assert.rejects( + async () => { + await client.verifyApiToken(); + }, + error => { + assert.ok(error instanceof Error); + assert.equal( + error.message, + "Failed to verify API token, got HTTP status code '401', expected '200'.", + ); + return true; + }, + ); + }); +}); diff --git a/test/writeEvents.test.ts b/test/writeEvents.test.ts new file mode 100644 index 0000000..4bc208b --- /dev/null +++ b/test/writeEvents.test.ts @@ -0,0 +1,160 @@ +import assert from 'node:assert/strict'; +import { afterEach, before, beforeEach, suite, test } from 'node:test'; +import { Client } from '../src/Client.js'; +import type { EventCandidate } from '../src/EventCandidate.js'; +import { isSubjectOnEventId } from '../src/isSubjectOnEventId.js'; +import { isSubjectPristine } from '../src/isSubjectPristine.js'; +import { EventSourcingDb } from './docker/EventSourcingDb.js'; + +suite('writeEvents', { timeout: 5_000 }, () => { + let eventSourcingDb: EventSourcingDb; + + before(() => { + EventSourcingDb.build(); + }); + + beforeEach(async () => { + eventSourcingDb = await EventSourcingDb.run(); + }); + + afterEach(() => { + eventSourcingDb.kill(); + }); + + test('writes a single event.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const event: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + const writtenEvents = await client.writeEvents([event]); + + assert.equal(writtenEvents.length, 1); + assert.equal(writtenEvents[0].id, '0'); + }); + + test('writes multiple events.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + const writtenEvents = await client.writeEvents([firstEvent, secondEvent]); + assert.equal(writtenEvents.length, 2); + + assert.equal(writtenEvents[0].id, '0'); + assert.equal(writtenEvents[0].data.value, 23); + + assert.equal(writtenEvents[1].id, '1'); + assert.equal(writtenEvents[1].data.value, 42); + }); + + test('supports the isSubjectPristine precondition.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + await client.writeEvents([firstEvent]); + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await assert.rejects( + async () => { + await client.writeEvents([secondEvent], [isSubjectPristine('/test')]); + }, + error => { + assert.ok(error instanceof Error); + assert.equal( + error.message, + "Failed to write events, got HTTP status code '409', expected '200'.", + ); + return true; + }, + ); + }); + + test('supports the isSubjectOnEventId precondition.', async (): Promise => { + const client = new Client( + new URL(`http://localhost:${eventSourcingDb.port}/`), + eventSourcingDb.apiToken, + ); + + const firstEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 23, + }, + }; + + await client.writeEvents([firstEvent]); + + const secondEvent: EventCandidate = { + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test', + data: { + value: 42, + }, + }; + + await assert.rejects( + async () => { + await client.writeEvents([secondEvent], [isSubjectOnEventId('/test', '1')]); + }, + error => { + assert.ok(error instanceof Error); + assert.equal( + error.message, + "Failed to write events, got HTTP status code '409', expected '200'.", + ); + return true; + }, + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index d44b26e..c91503f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,12 @@ "baseUrl": ".", "declaration": true, "esModuleInterop": true, - "lib": ["ES2022"], + "lib": ["ES2023"], "module": "CommonJS", "outDir": "dist", "resolveJsonModule": true, "strict": true, - "target": "ES2022" + "target": "ES2023" }, "include": ["./src/**/*.ts"], "exclude": ["./dist"]