diff --git a/package-lock.json b/package-lock.json index 4f50606c6..702ac500e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@adobe/spacecat-helix-content-sdk": "1.4.24", "@adobe/spacecat-shared-athena-client": "1.3.5", "@adobe/spacecat-shared-brand-client": "1.1.24", - "@adobe/spacecat-shared-data-access": "2.65.2", + "@adobe/spacecat-shared-data-access": "https://gitpkg.now.sh/adobe/spacecat-shared/packages/spacecat-shared-data-access?SITES-34129&t=20251004", "@adobe/spacecat-shared-gpt-client": "1.6.5", "@adobe/spacecat-shared-http-utils": "1.17.6", "@adobe/spacecat-shared-ims-client": "1.8.13", @@ -619,7 +619,6 @@ "resolved": "https://registry.npmjs.org/@adobe/helix-universal/-/helix-universal-5.2.3.tgz", "integrity": "sha512-13lpLUAyPvitjNh6eVZKjh8+kE+0LGHElyIaV0X7wOqUn371+BR9LrfS5seOpcIDAFNpEbL4T68rgFa1/aQrbA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@adobe/fetch": "4.2.3", "aws4": "1.13.2" @@ -4278,7 +4277,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.859.0.tgz", "integrity": "sha512-Bt840uICsGcn7IFewif8ARCF0CxtdTx9DX/LfUGRI+SVZcqyeEccmH2JJRRzThtEzKTXr+rCN6yaNB3c4RQY2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -5824,7 +5822,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -7100,7 +7097,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -8394,7 +8390,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9174,7 +9169,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -9247,8 +9241,8 @@ }, "node_modules/@adobe/spacecat-shared-data-access": { "version": "2.65.2", - "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-data-access/-/spacecat-shared-data-access-2.65.2.tgz", - "integrity": "sha512-9AVc/a67q3Pdpgt6ToRlXxmLUJiop4hXgY0jL961PTgYYo2wMg26C/uzwPyY7zmtEpLMS/JdeygQtqe4fYpTZg==", + "resolved": "https://gitpkg.now.sh/adobe/spacecat-shared/packages/spacecat-shared-data-access?SITES-34129&t=20251004", + "integrity": "sha512-py7485j3GFeSKVlSYPlg76XmlNDxa9mHZ4CQH9ib8HJlhLge4GDVeHWwcgbI8W7BBvFfFzVpFgpgY5EE8mH5fA==", "license": "Apache-2.0", "dependencies": { "@adobe/spacecat-shared-utils": "1.49.0", @@ -9320,7 +9314,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.859.0.tgz", "integrity": "sha512-Bt840uICsGcn7IFewif8ARCF0CxtdTx9DX/LfUGRI+SVZcqyeEccmH2JJRRzThtEzKTXr+rCN6yaNB3c4RQY2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -12841,7 +12834,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -13926,7 +13918,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -14334,7 +14325,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.721.0.tgz", "integrity": "sha512-jwsgdUEbNJqs1O0AQtf9M6SI7hFIjxH+IKeKCMca0xVt+Tr1UqLr/qMK/6W8LoMtRFnE0lpBSHW6hvmLp2OCoQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -14388,7 +14378,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.721.0.tgz", "integrity": "sha512-1Pv8F02hQFmPZs7WtGfQNlnInbG1lLzyngJc/MlZ3Ld2fIoWjaWp7bJWgYAjnzHNEuDtCabWJvIfePdRqsbYoA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -15772,7 +15761,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -16578,7 +16566,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -17532,7 +17519,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.859.0.tgz", "integrity": "sha512-Bt840uICsGcn7IFewif8ARCF0CxtdTx9DX/LfUGRI+SVZcqyeEccmH2JJRRzThtEzKTXr+rCN6yaNB3c4RQY2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -18935,7 +18921,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -20187,7 +20172,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -20559,7 +20543,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -21144,7 +21127,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -22937,7 +22919,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.859.0.tgz", "integrity": "sha512-Bt840uICsGcn7IFewif8ARCF0CxtdTx9DX/LfUGRI+SVZcqyeEccmH2JJRRzThtEzKTXr+rCN6yaNB3c4RQY2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -24483,7 +24464,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -25759,7 +25739,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -26701,7 +26680,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.859.0.tgz", "integrity": "sha512-Bt840uICsGcn7IFewif8ARCF0CxtdTx9DX/LfUGRI+SVZcqyeEccmH2JJRRzThtEzKTXr+rCN6yaNB3c4RQY2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -29426,7 +29404,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -30011,7 +29988,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -30346,7 +30322,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -30931,7 +30906,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -32562,7 +32536,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.859.0.tgz", "integrity": "sha512-Bt840uICsGcn7IFewif8ARCF0CxtdTx9DX/LfUGRI+SVZcqyeEccmH2JJRRzThtEzKTXr+rCN6yaNB3c4RQY2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -33143,7 +33116,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -34466,7 +34438,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.2.tgz", "integrity": "sha512-hAFEB+Stqm4FoQmIuyw5AzGVJh3BSfvLjK7IK4YYRXXLt1Oq9KS6pv2samYgRTTTXsxhmVpDjiYF3Xo/gfXIXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -35381,7 +35352,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.893.0.tgz", "integrity": "sha512-rojJDqmrpzwVfcE+EZ1Q1dnSWhHZQYwk9GinsvvnQNDCrARMyH+Q24jSLXk9rKSiY9UUj0P5o3kH2RdCOS3rGw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -37641,7 +37611,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.726.0.tgz", "integrity": "sha512-5JzTX9jwev7+y2Jkzjz0pd1wobB5JQfPOQF3N2DrJ5Pao0/k6uRYwE4NqB0p0HlGrMTDm7xNq7OSPPIPG575Jw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -39272,7 +39241,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -39295,7 +39263,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -40449,7 +40416,6 @@ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.77.tgz", "integrity": "sha512-aqXHea9xfpVn6VoCq9pjujwFqrh3vw3Fgm9KFUZJ1cF7Bx5HI62DvQPw8LlRB3NB4dhwBBA1ldAVkkkd1du8nA==", "license": "MIT", - "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", @@ -40663,7 +40629,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.4.tgz", "integrity": "sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -40979,7 +40944,6 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -41143,7 +41107,6 @@ "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -43088,7 +43051,6 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -43578,7 +43540,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -43625,7 +43586,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -44102,7 +44062,6 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.3.tgz", "integrity": "sha512-bltsLAr4juMJJ2tT5/L/CtwUGIvHihtPe6SO/z3jjOD73PHhOYxcuwCMFFyTbTy5S4WThJO32oZk7r+pg3ZoCQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -44427,7 +44386,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -44676,7 +44634,6 @@ "integrity": "sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -46793,7 +46750,6 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -49947,7 +49903,6 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -50905,7 +50860,6 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -51948,7 +51902,6 @@ "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -54971,7 +54924,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -55524,7 +55476,6 @@ "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz", "integrity": "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==", "license": "Apache-2.0", - "peer": true, "bin": { "openai": "bin/cli" }, @@ -56602,7 +56553,6 @@ "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -56613,7 +56563,6 @@ "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -57252,7 +57201,6 @@ "integrity": "sha512-phCkJ6pjDi9ANdhuF5ElS10GGdAKY6R1Pvt9lT3SFhOwM4T7QZE7MLpBDbNruUx/Q3gFD92/UOFringGipRqZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -58019,7 +57967,6 @@ "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.5", @@ -58605,7 +58552,6 @@ "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -59282,7 +59228,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -59705,7 +59650,6 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -60606,7 +60550,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -60904,7 +60847,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -60914,7 +60856,6 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "license": "ISC", - "peer": true, "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index 67a560d59..2bfb56d17 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build": "hedy -v --test-bundle", "deploy": "hedy -v --deploy --aws-deploy-bucket=spacecat-prod-deploy --pkgVersion=latest", "deploy-stage": "hedy -v --deploy --aws-deploy-bucket=spacecat-stage-deploy --pkgVersion=latest", - "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l latest --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", + "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l sandsinh --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", "deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env", "docs": "npm run docs:lint && npm run docs:build", "docs:build": "npx @redocly/cli build-docs -o ./docs/index.html --config docs/openapi/redocly-config.yaml", @@ -74,7 +74,7 @@ "@adobe/spacecat-helix-content-sdk": "1.4.24", "@adobe/spacecat-shared-athena-client": "1.3.5", "@adobe/spacecat-shared-brand-client": "1.1.24", - "@adobe/spacecat-shared-data-access": "2.65.2", + "@adobe/spacecat-shared-data-access": "https://gitpkg.now.sh/adobe/spacecat-shared/packages/spacecat-shared-data-access?SITES-34129&t=20251004", "@adobe/spacecat-shared-gpt-client": "1.6.5", "@adobe/spacecat-shared-http-utils": "1.17.6", "@adobe/spacecat-shared-ims-client": "1.8.13", diff --git a/src/controllers/fixes.js b/src/controllers/fixes.js index fcc8eed7e..4b49734e8 100644 --- a/src/controllers/fixes.js +++ b/src/controllers/fixes.js @@ -188,9 +188,13 @@ export class FixesController { const FixEntity = this.#FixEntity; const fixes = await Promise.all(context.data.map(async (fixData, index) => { try { + const fixEntity = await FixEntity.create({ ...fixData, opportunityId }); + if (fixData.suggestionIds) { + await FixEntity.setSuggestionsByFixEntityId(fixEntity.getId(), fixData.suggestionIds); + } return { index, - fix: FixDto.toJSON(await FixEntity.create({ ...fixData, opportunityId })), + fix: FixDto.toJSON(fixEntity), statusCode: 201, }; } catch (error) { @@ -326,10 +330,7 @@ export class FixesController { if (suggestions.some((s) => !s || s.getOpportunityId() !== opportunityId)) { return badRequest('Invalid suggestion IDs'); } - for (const suggestion of suggestions) { - suggestion.setFixEntityId(fixId); - } - await Promise.all(suggestions.map((s) => s.save())); + await this.#FixEntity.setSuggestionsByFixEntityId(fixId, suggestionIds); hasUpdates = true; } diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index 12b837660..6297e5db6 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -27,6 +27,7 @@ import { import { ValidationError, Suggestion as SuggestionModel, Site as SiteModel } from '@adobe/spacecat-shared-data-access'; import { SuggestionDto } from '../dto/suggestion.js'; +import { FixDto } from '../dto/fix.js'; import { sendAutofixMessage, getCSPromiseToken, ErrorWithStatusCode } from '../support/utils.js'; import AccessControlUtil from '../support/access-control-util.js'; @@ -329,6 +330,45 @@ function SuggestionsController(ctx, sqs, env) { return badRequest('No updates provided'); }; + /** + * Gets all fixes for a given suggestion + * @param {Object} context of the request + * @returns {Promise} Array of fixes response. + */ + const getSuggestionFixes = async (context) => { + const siteId = context.params?.siteId; + const opportunityId = context.params?.opportunityId; + const suggestionId = context.params?.suggestionId; + + if (!isValidUUID(siteId)) { + return badRequest('Site ID required'); + } + + if (!isValidUUID(opportunityId)) { + return badRequest('Opportunity ID required'); + } + + if (!isValidUUID(suggestionId)) { + return badRequest('Suggestion ID required'); + } + + try { + const site = await Site.findById(siteId); + if (!site) { + return notFound('Site not found'); + } + + if (!await accessControlUtil.hasAccess(site)) { + return forbidden('User does not belong to the organization'); + } + + const fixes = await Suggestion.getFixEntitiesBySuggestionId(suggestionId); + return ok({ data: fixes.map((fix) => FixDto.toJSON(fix)) }); + } catch (error) { + return createResponse({ message: 'Error retrieving fixes for suggestion' }, 500); + } + }; + /** * Update the status of one or multiple suggestions in one transaction * @param {Object} context of the request @@ -663,6 +703,7 @@ function SuggestionsController(ctx, sqs, env) { getAllForOpportunity, getByID, getByStatus, + getSuggestionFixes, patchSuggestion, patchSuggestionsStatus, removeSuggestion, diff --git a/src/routes/index.js b/src/routes/index.js index 3fe7501ab..70e9c6bde 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -170,6 +170,7 @@ export default function getRouteHandlers( 'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/auto-fix': suggestionsController.autofixSuggestions, 'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status': suggestionsController.getByStatus, 'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId': suggestionsController.getByID, + 'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId/fixes': suggestionsController.getSuggestionFixes, 'POST /sites/:siteId/opportunities/:opportunityId/suggestions': suggestionsController.createSuggestions, 'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/status': suggestionsController.patchSuggestionsStatus, 'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId': suggestionsController.patchSuggestion, diff --git a/test/controllers/fixes.test.js b/test/controllers/fixes.test.js index 4df3a78a7..177340484 100644 --- a/test/controllers/fixes.test.js +++ b/test/controllers/fixes.test.js @@ -54,6 +54,8 @@ describe('Fixes Controller', () => { let fixesController; /** @type {FixEntityCollection} */ let fixEntityCollection; + /** @type {FixEntitySuggestionCollection} */ + let fixEntitySuggestionCollection; /** @type {SuggestionCollection} */ let suggestionCollection; /** @type {RequestContext} */ @@ -79,11 +81,19 @@ describe('Fixes Controller', () => { const dataAccess = entityRegistry.getCollections(); fixEntityCollection = dataAccess.FixEntity; suggestionCollection = dataAccess.Suggestion; + fixEntitySuggestionCollection = dataAccess.FixEntitySuggestion; sandbox.stub(fixEntityCollection, 'allByOpportunityId'); sandbox.stub(fixEntityCollection, 'allByOpportunityIdAndStatus'); sandbox.stub(fixEntityCollection, 'findById'); + sandbox.stub(fixEntityCollection, 'setSuggestionsByFixEntityId'); sandbox.stub(suggestionCollection, 'allByIndexKeys'); sandbox.stub(suggestionCollection, 'findById'); + sandbox.stub(fixEntitySuggestionCollection, 'createMany'); + sandbox.stub(fixEntitySuggestionCollection, 'allByIndexKeys'); + sandbox.stub(fixEntitySuggestionCollection, 'removeByIndexKeys'); + sandbox.stub(fixEntitySuggestionCollection, 'allByFixEntityId'); + sandbox.stub(fixEntitySuggestionCollection, 'allBySuggestionId'); + sandbox.stub(suggestionCollection, 'batchGetByKeys'); sandbox.stub(dataAccess.Site.entity, 'get').returns({ go: async () => ({ data: { siteId } }), }); @@ -376,10 +386,14 @@ describe('Fixes Controller', () => { suggestionCollection.create({ opportunityId }), suggestionCollection.create({ opportunityId }), ]); - suggestionCollection.allByIndexKeys - .withArgs({ fixEntityId: fixId }) - .resolves(suggestions); - + suggestionCollection.batchGetByKeys.resolves({ + data: suggestions, + unprocessed: [], + }); + fixEntitySuggestionCollection.allByFixEntityId.resolves(suggestions.map((s) => ({ + getSuggestionId: () => s.getId(), + getFixEntityId: () => fixId, + }))); const response = await fixesController.getAllSuggestionsForFix(requestContext); expect(response).includes({ status: 200 }); expect(await response.json()).deep.equals(suggestions.map(SuggestionDto.toJSON)); @@ -419,8 +433,27 @@ describe('Fixes Controller', () => { }); describe('create fixes', () => { + async function createSuggestion(options) { + options.opportunityId ??= opportunityId; + options.status ??= 'PENDING'; + + const suggestion = await suggestionCollection.create(options); + suggestionCollection.findById + .withArgs(suggestion.getId()) + .resolves(suggestion); + + return suggestion; + } + beforeEach(() => { requestContext.data = null; + + // Configure the setSuggestionsByFixEntityId mock for create fixes tests + fixEntityCollection.setSuggestionsByFixEntityId.resolves({ + createdItems: [], + errorItems: [], + removedCount: 0, + }); }); it('responds 403 if the request does not have authorization/access', async () => { @@ -548,6 +581,113 @@ describe('Fixes Controller', () => { expect(response).includes({ status: 400 }); expect(await response.json()).deep.equals({ message: 'Opportunity ID required' }); }); + + it('can create a fix with suggestion IDs using many-to-many relationship', async () => { + const suggestions = await Promise.all([ + createSuggestion({ type: 'CONTENT_UPDATE' }), + createSuggestion({ type: 'REDIRECT_UPDATE' }), + ]); + + const fixData = { + type: 'CONTENT_UPDATE', + opportunityId, + suggestionIds: suggestions.map((s) => s.getId()), + }; + requestContext.data = [fixData]; + + // Configure the setSuggestionsByFixEntityId method for this test + fixEntityCollection.setSuggestionsByFixEntityId.resolves({ + createdItems: suggestions.map((s) => ({ + getSuggestionId: () => s.getId(), + getFixEntityId: () => 'mock-fix-id', + })), + errorItems: [], + removedCount: 0, + }); + + const response = await fixesController.createFixes(requestContext); + expect(response).includes({ status: 207 }); + + const { fixes, metadata } = await response.json(); + expect(metadata).deep.equals({ total: 1, success: 1, failed: 0 }); + expect(fixes).have.lengthOf(1); + expect(fixes[0]).includes({ index: 0, statusCode: 201 }); + expect(fixes[0].fix).includes({ type: 'CONTENT_UPDATE', opportunityId }); + + // Verify that setSuggestionsByFixEntityId was called + expect(fixEntityCollection.setSuggestionsByFixEntityId).to.have.been.calledOnce; + }); + + it('can create multiple fixes with different suggestion IDs', async () => { + const suggestions1 = await Promise.all([ + createSuggestion({ type: 'CONTENT_UPDATE' }), + createSuggestion({ type: 'REDIRECT_UPDATE' }), + ]); + + const suggestions2 = await Promise.all([ + createSuggestion({ type: 'METADATA_UPDATE' }), + ]); + + const fixData = [ + { + type: 'CONTENT_UPDATE', + opportunityId, + suggestionIds: suggestions1.map((s) => s.getId()), + }, + { + type: 'REDIRECT_UPDATE', + opportunityId, + suggestionIds: suggestions2.map((s) => s.getId()), + }, + { + type: 'METADATA_UPDATE', + opportunityId, + // No suggestionIds for this one + }, + ]; + requestContext.data = fixData; + + // Configure the setSuggestionsByFixEntityId method for this test + fixEntityCollection.setSuggestionsByFixEntityId.resolves({ + createdItems: [], + errorItems: [], + removedCount: 0, + }); + + const response = await fixesController.createFixes(requestContext); + expect(response).includes({ status: 207 }); + + const { fixes, metadata } = await response.json(); + expect(metadata).deep.equals({ total: 3, success: 3, failed: 0 }); + expect(fixes).have.lengthOf(3); + + expect(fixEntityCollection.setSuggestionsByFixEntityId).to.have.been.calledTwice; + }); + + it('handles invalid suggestion IDs during fix creation', async () => { + const validSuggestion = await createSuggestion({ type: 'CONTENT_UPDATE' }); + const invalidSuggestionId = '15345195-62e6-494c-81b1-1d0da0b51d84'; + + const fixData = { + type: 'CONTENT_UPDATE', + opportunityId, + suggestionIds: [validSuggestion.getId(), invalidSuggestionId], + }; + requestContext.data = [fixData]; + + // Configure validation failure in setSuggestionsByFixEntityId + suggestionCollection.findById.withArgs(invalidSuggestionId).resolves(null); + fixEntityCollection.setSuggestionsByFixEntityId.rejects(new Error('Invalid suggestion IDs')); + + const response = await fixesController.createFixes(requestContext); + expect(response).includes({ status: 207 }); + + const { fixes, metadata } = await response.json(); + expect(metadata).deep.equals({ total: 1, success: 0, failed: 1 }); + expect(fixes).have.lengthOf(1); + expect(fixes[0]).includes({ index: 0, statusCode: 500 }); + expect(fixes[0].message).to.include('Invalid suggestion IDs'); + }); }); describe('patch fix status', () => { @@ -765,14 +905,12 @@ describe('Fixes Controller', () => { options.status ??= 'PENDING'; const suggestion = await suggestionCollection.create(options); - setSuggestionFix(suggestion, suggestion.getFixEntityId()); + // No longer setting fixEntityId directly on suggestions suggestionCollection.findById .withArgs(suggestion.getId()) .resolves(suggestion); - sinon.stub(suggestion.patcher, 'save').callsFake(() => { - setSuggestionFix(suggestion, suggestion.getFixEntityId()); - }); + sinon.stub(suggestion.patcher, 'save'); return suggestion; } @@ -801,6 +939,31 @@ describe('Fixes Controller', () => { suggestionCollection.allByIndexKeys.callsFake( async ({ fixEntityId: id }) => [...(suggestionsByFix.get(id) ?? [])], ); + + // Configure the many-to-many relationship method mock + fixEntityCollection.setSuggestionsByFixEntityId.callsFake( + async (fixId, suggestionIds) => { + // Clear existing relationships for this fix + suggestionsByFix.set(fixId, new Set()); + + // Set new relationships + for (const suggestionId of suggestionIds) { + // eslint-disable-next-line no-await-in-loop + const suggestion = await suggestionCollection.findById(suggestionId); + if (suggestion) { + setSuggestionFix(suggestion, fixId); + } + } + + return { + createdItems: suggestionIds + .map((id) => ({ getSuggestionId: () => id, getFixEntityId: () => fixId })), + errorItems: [], + removedCount: 0, + }; + }, + ); + fix = await fixEntityCollection.create({ fixEntityId, type: Suggestion.TYPES.CONTENT_UPDATE, @@ -827,6 +990,16 @@ describe('Fixes Controller', () => { createSuggestion({ type: 'REDIRECT_UPDATE' }), ]); + fixEntitySuggestionCollection.allByFixEntityId.resolves(suggestions.map((s) => ({ + getSuggestionId: () => s.getId(), + getFixEntityId: () => fix.getId(), + }))); + + suggestionCollection.batchGetByKeys.resolves({ + data: suggestions, + unprocessed: [], + }); + const executedAt = '2025-05-19T10:27:27.903Z'; const publishedAt = '2025-05-19T11:27:27.903Z'; const changeDetails = { arbitrary: 'Changes' }; @@ -921,6 +1094,91 @@ describe('Fixes Controller', () => { message: 'Opportunity not found', }); }); + + it('can patch a fix with empty suggestion IDs array', async () => { + requestContext.data = { + suggestionIds: [], + changeDetails: { arbitrary: 'Changes' }, + }; + + const response = await fixesController.patchFix(requestContext); + expect(response).includes({ status: 200 }); + + // Verify that setSuggestionsByFixEntityId was called with empty array + expect(fixEntityCollection.setSuggestionsByFixEntityId) + .to.have.been.calledWith(fixEntityId, []); + }); + + it('can patch a fix with only suggestion IDs (no other updates)', async () => { + const suggestions = await Promise.all([ + createSuggestion({ type: 'CONTENT_UPDATE' }), + ]); + + requestContext.data = { + suggestionIds: suggestions.map((s) => s.getId()), + }; + + const response = await fixesController.patchFix(requestContext); + expect(response).includes({ status: 200 }); + + // Verify that setSuggestionsByFixEntityId was called + expect(fixEntityCollection.setSuggestionsByFixEntityId).to.have.been.calledWith( + fixEntityId, + suggestions.map((s) => s.getId()), + ); + }); + + it('handles setSuggestionsByFixEntityId failure gracefully', async () => { + const suggestions = await Promise.all([ + createSuggestion({ type: 'CONTENT_UPDATE' }), + ]); + + // Configure setSuggestionsByFixEntityId to throw an error + fixEntityCollection.setSuggestionsByFixEntityId.rejects(new Error('Database error')); + sandbox.stub(log, 'error'); // silence error logging + + requestContext.data = { + suggestionIds: suggestions.map((s) => s.getId()), + changeDetails: { arbitrary: 'Changes' }, + }; + + const response = await fixesController.patchFix(requestContext); + expect(response).includes({ status: 500 }); + expect(await response.json()).deep.equals({ + message: 'Error updating fix', + }); + }); + + it('can patch a fix with mixed updates including suggestion IDs', async () => { + const suggestions = await Promise.all([ + createSuggestion({ type: 'CONTENT_UPDATE' }), + createSuggestion({ type: 'REDIRECT_UPDATE' }), + ]); + + const executedAt = '2025-05-19T10:27:27.903Z'; + const changeDetails = { arbitrary: 'Changes' }; + + requestContext.data = { + executedBy: 'updated-user', + executedAt, + changeDetails, + suggestionIds: suggestions.map((s) => s.getId()), + }; + + const response = await fixesController.patchFix(requestContext); + expect(response).includes({ status: 200 }); + + // Verify all updates were applied + expect(fix.getExecutedBy()).equals('updated-user'); + expect(fix.getExecutedAt()).equals(executedAt); + expect(fix.getChangeDetails()).deep.equals(changeDetails); + + // Verify that setSuggestionsByFixEntityId was called + expect(fixEntityCollection.setSuggestionsByFixEntityId).to.have.been.calledWith( + fixEntityId, + suggestions.map((s) => s.getId()), + ); + }); }); describe('fix removal', () => { diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index ac52ea9d0..e39aa36f3 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -145,6 +145,7 @@ describe('Suggestions Controller', () => { 'getAllForOpportunity', 'getByID', 'getByStatus', + 'getSuggestionFixes', 'patchSuggestion', 'patchSuggestionsStatus', 'removeSuggestion', @@ -708,6 +709,168 @@ describe('Suggestions Controller', () => { expect(error).to.have.property('message', 'not found'); }); + describe('getSuggestionFixes', () => { + const FIX_IDS = [ + 'fix-id-1', + 'fix-id-2', + 'fix-id-3', + ]; + + const mockFixEntities = [ + { + getId: () => FIX_IDS[0], + getOpportunityId: () => OPPORTUNITY_ID, + getType: () => 'CODE_CHANGE', + getCreatedAt: () => '2025-01-01T00:00:00.000Z', + getExecutedAt: () => '2025-01-01T01:00:00.000Z', + getPublishedAt: () => '2025-01-01T02:00:00.000Z', + getChangeDetails: () => ({ file: 'index.js', changes: 'updated' }), + getStatus: () => 'COMPLETED', + }, + { + getId: () => FIX_IDS[1], + getOpportunityId: () => OPPORTUNITY_ID, + getType: () => 'CONTENT_UPDATE', + getCreatedAt: () => '2025-01-02T00:00:00.000Z', + getExecutedAt: () => '2025-01-02T01:00:00.000Z', + getPublishedAt: () => null, + getChangeDetails: () => ({ content: 'new content' }), + getStatus: () => 'IN_PROGRESS', + }, + ]; + + beforeEach(() => { + mockSuggestion.getFixEntitiesBySuggestionId = sandbox.stub().resolves(mockFixEntities); + }); + + it('gets all fixes for a suggestion successfully', async () => { + const response = await suggestionsController.getSuggestionFixes({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(200); + const result = await response.json(); + expect(result).to.have.property('data'); + expect(result.data).to.be.an('array').with.lengthOf(2); + expect(result.data[0]).to.have.property('id', FIX_IDS[0]); + expect(result.data[0]).to.have.property('type', 'CODE_CHANGE'); + expect(result.data[0]).to.have.property('status', 'COMPLETED'); + expect(result.data[1]).to.have.property('id', FIX_IDS[1]); + expect(result.data[1]).to.have.property('type', 'CONTENT_UPDATE'); + expect(mockSuggestion.getFixEntitiesBySuggestionId) + .to.have.been.calledOnceWith(SUGGESTION_IDS[0]); + }); + + it('returns empty array when suggestion has no fixes', async () => { + mockSuggestion.getFixEntitiesBySuggestionId.resolves([]); + const response = await suggestionsController.getSuggestionFixes({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(200); + const result = await response.json(); + expect(result).to.have.property('data'); + expect(result.data).to.be.an('array').with.lengthOf(0); + }); + + it('returns bad request if no site ID is passed', async () => { + const response = await suggestionsController.getSuggestionFixes({ + params: { + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(400); + const error = await response.json(); + expect(error).to.have.property('message', 'Site ID required'); + expect(mockSuggestion.getFixEntitiesBySuggestionId).to.not.have.been.called; + }); + + it('returns bad request if no opportunity ID is passed', async () => { + const response = await suggestionsController.getSuggestionFixes({ + params: { + siteId: SITE_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(400); + const error = await response.json(); + expect(error).to.have.property('message', 'Opportunity ID required'); + expect(mockSuggestion.getFixEntitiesBySuggestionId).to.not.have.been.called; + }); + + it('returns bad request if no suggestion ID is passed', async () => { + const response = await suggestionsController.getSuggestionFixes({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + ...context, + }); + expect(response.status).to.equal(400); + const error = await response.json(); + expect(error).to.have.property('message', 'Suggestion ID required'); + expect(mockSuggestion.getFixEntitiesBySuggestionId).to.not.have.been.called; + }); + + it('returns not found if site does not exist', async () => { + const response = await suggestionsController.getSuggestionFixes({ + params: { + siteId: SITE_ID_NOT_FOUND, + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(404); + const error = await response.json(); + expect(error).to.have.property('message', 'Site not found'); + expect(mockSuggestion.getFixEntitiesBySuggestionId).to.not.have.been.called; + }); + + it('returns forbidden if user does not belong to the organization', async () => { + sandbox.stub(AccessControlUtil.prototype, 'hasAccess').returns(false); + sandbox.stub(context.attributes.authInfo, 'hasOrganization').returns(false); + const response = await suggestionsController.getSuggestionFixes({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(403); + const error = await response.json(); + expect(error).to.have.property('message', 'User does not belong to the organization'); + expect(mockSuggestion.getFixEntitiesBySuggestionId).to.not.have.been.called; + }); + + it('returns 500 error if getFixEntitiesBySuggestionId throws an error', async () => { + mockSuggestion.getFixEntitiesBySuggestionId.rejects(new Error('Database error')); + const response = await suggestionsController.getSuggestionFixes({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(500); + const error = await response.json(); + expect(error).to.have.property('message', 'Error retrieving fixes for suggestion'); + }); + }); + it('creates 2 suggestions success', async () => { const response = await suggestionsController.createSuggestions({ params: { diff --git a/test/routes/index.test.js b/test/routes/index.test.js index 44079b0e1..df211a840 100755 --- a/test/routes/index.test.js +++ b/test/routes/index.test.js @@ -354,6 +354,7 @@ describe('getRouteHandlers', () => { 'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId', 'DELETE /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId', 'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/status', + 'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId/fixes', 'GET /sites/:siteId/scraped-content/:type', 'GET /sites/:siteId/top-pages', 'GET /sites/:siteId/top-pages/:source',