From 12ba55729b6f0fea2a704fb2e5302db9deadd661 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 11 Sep 2020 14:37:17 +0000 Subject: [PATCH 1/4] Unwire Gateway and Federation from the repository tooling, prior to moving. This is a precursor to a step that will remove this code which has been moved to another repository. See https://github.com/apollographql/apollo-server/issues/4560 for details, and follow the next commit for the removal! --- package-lock.json | 628 +++----------------------------------------- package.json | 5 - tsconfig.build.json | 2 - 3 files changed, 36 insertions(+), 599 deletions(-) diff --git a/package-lock.json b/package-lock.json index a83360c69b7..da904d5a5f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,504 +3,6 @@ "requires": true, "lockfileVersion": 1, "dependencies": { - "@apollo/federation": { - "version": "file:packages/apollo-federation", - "requires": { - "apollo-graphql": "^0.6.0", - "apollo-server-env": "file:packages/apollo-server-env", - "core-js": "^3.4.0", - "lodash.xorby": "^4.7.0" - }, - "dependencies": { - "@types/node-fetch": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", - "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "apollo-env": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.5.tgz", - "integrity": "sha512-jeBUVsGymeTHYWp3me0R2CZRZrFeuSZeICZHCeRflHTfnQtlmbSXdy5E0pOyRM9CU4JfQkKDC98S1YglQj7Bzg==", - "requires": { - "@types/node-fetch": "2.5.7", - "core-js": "^3.0.1", - "node-fetch": "^2.2.0", - "sha.js": "^2.4.11" - } - }, - "apollo-graphql": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.6.0.tgz", - "integrity": "sha512-BxTf5LOQe649e9BNTPdyCGItVv4Ll8wZ2BKnmiYpRAocYEXAVrQPWuSr3dO4iipqAU8X0gvle/Xu9mSqg5b7Qg==", - "requires": { - "apollo-env": "^0.6.5", - "lodash.sortby": "^4.7.0" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "core-js": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", - "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==" - }, - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "@apollo/gateway": { - "version": "file:packages/apollo-gateway", - "requires": { - "@apollo/federation": "file:packages/apollo-federation", - "@types/node-fetch": "2.5.4", - "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", - "apollo-env": "^0.6.1", - "apollo-graphql": "^0.6.0", - "apollo-server-caching": "file:packages/apollo-server-caching", - "apollo-server-core": "file:packages/apollo-server-core", - "apollo-server-env": "file:packages/apollo-server-env", - "apollo-server-errors": "file:packages/apollo-server-errors", - "apollo-server-types": "file:packages/apollo-server-types", - "graphql-extensions": "file:packages/graphql-extensions", - "loglevel": "^1.6.1", - "make-fetch-happen": "^8.0.0", - "pretty-format": "^26.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.0.1.tgz", - "integrity": "sha512-IbtjvqI9+eS1qFnOIEL7ggWmT+iK/U+Vde9cGWtYb/b6XgKb3X44ZAe/z9YZzoAAZ/E92m0DqrilF934IGNnQA==", - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "@types/node-fetch": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz", - "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==", - "requires": { - "@types/node": "*" - } - }, - "agent-base": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", - "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", - "requires": { - "debug": "4" - } - }, - "agentkeepalive": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.0.tgz", - "integrity": "sha512-CW/n1wxF8RpEuuiq6Vbn9S8m0VSYDMnZESqaJ6F2cWN9fY8rei2qaxweIaRgq+ek8TqfoFIsUjaGNKGGEHElSg==", - "requires": { - "debug": "^4.1.0", - "depd": "^1.1.2", - "humanize-ms": "^1.2.1" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "apollo-graphql": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.6.0.tgz", - "integrity": "sha512-BxTf5LOQe649e9BNTPdyCGItVv4Ll8wZ2BKnmiYpRAocYEXAVrQPWuSr3dO4iipqAU8X0gvle/Xu9mSqg5b7Qg==", - "requires": { - "apollo-env": "^0.6.5", - "lodash.sortby": "^4.7.0" - }, - "dependencies": { - "@types/node-fetch": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", - "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "apollo-env": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.5.tgz", - "integrity": "sha512-jeBUVsGymeTHYWp3me0R2CZRZrFeuSZeICZHCeRflHTfnQtlmbSXdy5E0pOyRM9CU4JfQkKDC98S1YglQj7Bzg==", - "requires": { - "@types/node-fetch": "2.5.7", - "core-js": "^3.0.1", - "node-fetch": "^2.2.0", - "sha.js": "^2.4.11" - } - } - } - }, - "cacache": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.0.tgz", - "integrity": "sha512-L0JpXHhplbJSiDGzyJJnJCTL7er7NzbBgxzVqLswEb4bO91Zbv17OUMuUeu/q0ZwKn3V+1HM4wb9tO4eVE/K8g==", - "requires": { - "chownr": "^1.1.2", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "move-concurrently": "^1.0.1", - "p-map": "^3.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^2.7.1", - "ssri": "^8.0.0", - "tar": "^6.0.1", - "unique-filename": "^1.1.1" - }, - "dependencies": { - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", - "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", - "requires": { - "minipass": "^3.0.0" - } - } - } - }, - "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, - "make-fetch-happen": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.4.tgz", - "integrity": "sha512-hIFoqGq1db0QMiy/Atr/pI1Rs4rDV+ZdGSey2SQyF3KK3u1z4aj9mS5UdNnZkdQpA+H3pGn0J3KlEwsi2x4EqA==", - "requires": { - "agentkeepalive": "^4.1.0", - "cacache": "^15.0.0", - "http-cache-semantics": "^4.0.4", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^5.1.1", - "minipass": "^3.0.0", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.1.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "promise-retry": "^1.1.1", - "socks-proxy-agent": "^5.0.0", - "ssri": "^8.0.0" - }, - "dependencies": { - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-fetch": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.2.1.tgz", - "integrity": "sha512-ssHt0dkljEDaKmTgQ04DQgx2ag6G2gMPxA5hpcsoeTbfDgRf2fC2gNSRc6kISjD7ckCpHwwQvXxuTBK8402fXg==", - "requires": { - "encoding": "^0.1.12", - "minipass": "^3.1.0", - "minipass-pipeline": "^1.2.2", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", - "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", - "requires": { - "minipass": "^3.0.0" - } - }, - "socks-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz", - "integrity": "sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==", - "requires": { - "agent-base": "6", - "debug": "4", - "socks": "^2.3.3" - } - } - } - }, - "minipass": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", - "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", - "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "requires": { - "aggregate-error": "^3.0.0" - }, - "dependencies": { - "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - } - } - }, - "pretty-format": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.0.1.tgz", - "integrity": "sha512-SWxz6MbupT3ZSlL0Po4WF/KujhQaVehijR2blyRDCzk9e45EaYMVhMBn49fnRuHxtkSpXTes1GxNpVmH86Bxfw==", - "requires": { - "@jest/types": "^26.0.1", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "ssri": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", - "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", - "requires": { - "minipass": "^3.1.1" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "tar": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.1.tgz", - "integrity": "sha512-bKhKrrz2FJJj5s7wynxy/fyxpE0CmCjmOQ1KV4KkgXFWOgoIT/NbTMnB1n+LFNrNk0SSBVGGxcK5AGsyC+pW5Q==", - "requires": { - "chownr": "^1.1.3", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.0", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, "@apollo/protobufjs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.0.3.tgz", @@ -4961,7 +4463,8 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true }, "@types/connect": { "version": "3.4.33", @@ -5116,12 +4619,14 @@ "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==" + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true }, "@types/istanbul-lib-report": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "dev": true, "requires": { "@types/istanbul-lib-coverage": "*" } @@ -5130,6 +4635,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, "requires": { "@types/istanbul-lib-coverage": "*", "@types/istanbul-lib-report": "*" @@ -5572,6 +5078,7 @@ "version": "15.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, "requires": { "@types/yargs-parser": "*" } @@ -5579,7 +5086,8 @@ "@types/yargs-parser": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==" + "integrity": "sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==", + "dev": true }, "@wry/equality": { "version": "0.1.9", @@ -5849,83 +5357,6 @@ "@apollo/protobufjs": "^1.0.3" } }, - "apollo-env": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.1.tgz", - "integrity": "sha512-B9BgpQGR1ndeDtb4Gtor0J4CITQ+OPACZrVW6lgStnljKEe9ZB76DZ1dAd3OCeizAswW6Lo9uvfK8jhVS5nBhQ==", - "requires": { - "@types/node-fetch": "2.5.4", - "core-js": "^3.0.1", - "node-fetch": "^2.2.0", - "sha.js": "^2.4.11" - }, - "dependencies": { - "@types/node-fetch": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz", - "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==", - "requires": { - "@types/node": "*" - } - } - } - }, - "apollo-federation-integration-testsuite": { - "version": "file:packages/apollo-federation-integration-testsuite", - "requires": { - "apollo-graphql": "^0.6.0", - "graphql-tag": "^2.10.4" - }, - "dependencies": { - "@types/node-fetch": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", - "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "apollo-env": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.5.tgz", - "integrity": "sha512-jeBUVsGymeTHYWp3me0R2CZRZrFeuSZeICZHCeRflHTfnQtlmbSXdy5E0pOyRM9CU4JfQkKDC98S1YglQj7Bzg==", - "requires": { - "@types/node-fetch": "2.5.7", - "core-js": "^3.0.1", - "node-fetch": "^2.2.0", - "sha.js": "^2.4.11" - } - }, - "apollo-graphql": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.6.0.tgz", - "integrity": "sha512-BxTf5LOQe649e9BNTPdyCGItVv4Ll8wZ2BKnmiYpRAocYEXAVrQPWuSr3dO4iipqAU8X0gvle/Xu9mSqg5b7Qg==", - "requires": { - "apollo-env": "^0.6.5", - "lodash.sortby": "^4.7.0" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, "apollo-fetch": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/apollo-fetch/-/apollo-fetch-0.7.0.tgz", @@ -6631,7 +6062,8 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true }, "are-we-there-yet": { "version": "1.1.5", @@ -7571,7 +7003,8 @@ "chownr": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "dev": true }, "ci-info": { "version": "2.0.0", @@ -8350,6 +7783,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, "requires": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", @@ -8378,7 +7812,8 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "cors": { "version": "2.8.5", @@ -9845,6 +9280,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, "requires": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", @@ -9855,12 +9291,14 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9875,6 +9313,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -10514,6 +9953,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -10563,7 +10003,8 @@ "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true }, "graphql": { "version": "14.7.0", @@ -11193,7 +10634,8 @@ "iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true }, "ignore": { "version": "4.0.6", @@ -17410,11 +16852,6 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, - "lodash.xorby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.xorby/-/lodash.xorby-4.7.0.tgz", - "integrity": "sha1-nBmm+fBjputT3QPBtocXmYAUY9c=" - }, "log4js": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", @@ -18085,6 +17522,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, "requires": { "minimist": "0.0.8" }, @@ -18092,7 +17530,8 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true } } }, @@ -18128,6 +17567,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, "requires": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", @@ -19071,7 +18511,8 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true }, "promise-inflight": { "version": "1.0.1", @@ -19704,6 +19145,7 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -19724,6 +19166,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, "requires": { "aproba": "^1.1.1" } @@ -21457,7 +20900,8 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, "util-promisify": { "version": "2.1.0", diff --git a/package.json b/package.json index 2fb61f2360a..e5ea8447b0c 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,6 @@ "watch": "tsc --build tsconfig.build.json --watch", "release:version-bump": "lerna version", "release:version-bump:server": "npm run release:version-bump -- --force-publish=apollo-server,apollo-server-core,apollo-server-azure-functions,apollo-server-cloud-functions,apollo-server-cloudflare,apollo-server-express,apollo-server-fastify,apollo-server-hapi,apollo-server-koa,apollo-server-lambda,apollo-server-micro,apollo-server-integration-testsuite,apollo-server-testing", - "release:version-bump:federation": "npm run release:version-bump -- --force-publish=@apollo/federation,@apollo/gateway,apollo-federation-integration-testsuite", - "release:version-bump:server-and-federation": "npm run release:version-bump -- --force-publish=@apollo/federation,@apollo/gateway,apollo-federation-integration-testsuite,apollo-server,apollo-server-core,apollo-server-azure-functions,apollo-server-cloud-functions,apollo-server-cloudflare,apollo-server-express,apollo-server-fastify,apollo-server-hapi,apollo-server-koa,apollo-server-lambda,apollo-server-micro,apollo-server-integration-testsuite,apollo-server-testing", "release:start-ci-publish": "node -p '`Publish (dist-tag:${process.env.APOLLO_DIST_TAG || \"latest\"})`' | git tag -F - \"publish/$(date -u '+%Y%m%d%H%M%S')\" && git push origin \"$(git describe --match='publish/*' --tags --exact-match HEAD)\"", "postinstall": "lerna run prepare && npm run compile", "test": "jest --verbose", @@ -27,14 +25,11 @@ }, "dependencies": { "@apollographql/apollo-tools": "0.4.8", - "@apollo/federation": "file:packages/apollo-federation", - "@apollo/gateway": "file:packages/apollo-gateway", "apollo-cache-control": "file:packages/apollo-cache-control", "apollo-datasource": "file:packages/apollo-datasource", "apollo-datasource-rest": "file:packages/apollo-datasource-rest", "apollo-engine-reporting": "file:packages/apollo-engine-reporting", "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", - "apollo-federation-integration-testsuite": "file:packages/apollo-federation-integration-testsuite", "apollo-server": "file:packages/apollo-server", "apollo-server-azure-functions": "file:packages/apollo-server-azure-functions", "apollo-server-cache-memcached": "file:packages/apollo-server-cache-memcached", diff --git a/tsconfig.build.json b/tsconfig.build.json index 8d4966f6bf9..200f17e3ebe 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -9,8 +9,6 @@ { "path": "./packages/apollo-datasource" }, { "path": "./packages/apollo-datasource-rest" }, { "path": "./packages/apollo-engine-reporting" }, - { "path": "./packages/apollo-federation" }, - { "path": "./packages/apollo-gateway" }, { "path": "./packages/apollo-server" }, { "path": "./packages/apollo-server-azure-functions" }, { "path": "./packages/apollo-server-cache-memcached" }, From 811b7576243c2fb82e11016dafce9c5294e18ec9 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 11 Sep 2020 14:25:43 +0000 Subject: [PATCH 2/4] Move Federation (including Gateway) to another repository. Everything will continue, however, as federation becomes more independent from Apollo Server, it (and its concerns, like Apollo Gateway) move to their own repository. (It's always been relatively independent, but it didn't necessarily warrant its own repository before.) This includes `apollo-federation-integration-testsuite` (unpublished to npm), `@apollo/gateway (which lived in `packages/apollo-gateway` here and as `/gateway-js` on the new repository), and `@apollo/federation` (which lived in `packages/apollo-federation` here and as `/federation-js` on the new repository). See https://github.com/apollographql/apollo-server/issues/4560 for details, and also https://github.com/apollographql/federation/pull/134 for where (and how) it was introduced on the other side. --- .../.npmignore | 6 - .../package.json | 27 - .../src/fixtures/accounts.ts | 158 -- .../src/fixtures/books.ts | 129 -- .../src/fixtures/documents.ts | 35 - .../src/fixtures/index.ts | 33 - .../src/fixtures/inventory.ts | 62 - .../src/fixtures/product.ts | 247 --- .../src/fixtures/reviews.ts | 241 --- .../src/index.ts | 1 - .../tsconfig.json | 13 - packages/apollo-federation/.npmignore | 6 - packages/apollo-federation/CHANGELOG.md | 177 -- packages/apollo-federation/LICENSE.md | 20 - packages/apollo-federation/README.md | 40 - packages/apollo-federation/jest.config.js | 19 - packages/apollo-federation/package.json | 25 - .../src/__tests__/tsconfig.json | 7 - .../src/composition/__tests__/compose.test.ts | 1363 -------------- .../__tests__/composeAndValidate.test.ts | 878 --------- .../composition/__tests__/normalize.test.ts | 410 ---- .../src/composition/__tests__/tsconfig.json | 8 - .../src/composition/__tests__/utils.test.ts | 88 - .../src/composition/compose.ts | 652 ------- .../src/composition/composeAndValidate.ts | 47 - .../src/composition/index.ts | 6 - .../src/composition/normalize.ts | 321 ---- .../src/composition/rules.ts | 59 - .../src/composition/types.ts | 102 - .../src/composition/utils.ts | 583 ------ .../validate/__tests__/tsconfig.json | 5 - .../src/composition/validate/index.ts | 59 - .../executableDirectivesIdentical.test.ts | 122 -- .../executableDirectivesInAllServices.test.ts | 91 - .../__tests__/externalMissingOnBase.test.ts | 92 - .../__tests__/externalTypeMismatch.test.ts | 76 - .../__tests__/externalUnused.test.ts | 380 ---- .../__tests__/keyFieldsMissingOnBase.test.ts | 113 -- .../keyFieldsSelectInvalidType.test.ts | 132 -- .../__tests__/keysMatchBaseService.test.ts | 117 -- .../providesFieldsMissingExternals.test.ts | 106 -- .../providesFieldsSelectInvalidType.test.ts | 214 --- .../__tests__/providesNotOnEntity.test.ts | 259 --- .../requiresFieldsMissingExternals.test.ts | 76 - .../requiresFieldsMissingOnBase.test.ts | 79 - .../postComposition/__tests__/tsconfig.json | 5 - .../executableDirectivesIdentical.ts | 60 - .../executableDirectivesInAllServices.ts | 60 - .../postComposition/externalMissingOnBase.ts | 62 - .../postComposition/externalTypeMismatch.ts | 65 - .../postComposition/externalUnused.ts | 237 --- .../validate/postComposition/index.ts | 28 - .../postComposition/keyFieldsMissingOnBase.ts | 52 - .../keyFieldsSelectInvalidType.ts | 85 - .../postComposition/keysMatchBaseService.ts | 86 - .../providesFieldsMissingExternal.ts | 60 - .../providesFieldsSelectInvalidType.ts | 106 -- .../postComposition/providesNotOnEntity.ts | 77 - .../requiresFieldsMissingExternal.ts | 57 - .../requiresFieldsMissingOnBase.ts | 54 - .../__tests__/duplicateEnumOrScalar.test.ts | 97 - .../__tests__/duplicateEnumValue.test.ts | 74 - .../__tests__/externalUsedOnBase.test.ts | 51 - .../keyFieldsMissingExternal.test.ts | 181 -- .../__tests__/requiresUsedOnBase.test.ts | 51 - .../__tests__/reservedFieldUsed.test.ts | 153 -- .../preComposition/__tests__/tsconfig.json | 5 - .../preComposition/duplicateEnumOrScalar.ts | 50 - .../preComposition/duplicateEnumValue.ts | 72 - .../preComposition/externalUsedOnBase.ts | 43 - .../validate/preComposition/index.ts | 6 - .../keyFieldsMissingExternal.ts | 115 -- .../preComposition/requiresUsedOnBase.ts | 43 - .../preComposition/reservedFieldUsed.ts | 48 - .../__tests__/rootFieldUsed.test.ts | 170 -- .../validate/preNormalization/index.ts | 1 - .../preNormalization/rootFieldUsed.ts | 81 - .../sdl/__tests__/matchingEnums.test.ts | 205 -- .../sdl/__tests__/matchingUnions.test.ts | 132 -- .../__tests__/possibleTypeExtensions.test.ts | 198 -- .../validate/sdl/__tests__/tsconfig.json | 5 - .../uniqueFieldDefinitionNames.test.ts | 268 --- .../uniqueTypeNamesWithFields.test.ts | 541 ------ .../src/composition/validate/sdl/index.ts | 5 - .../composition/validate/sdl/matchingEnums.ts | 123 -- .../validate/sdl/matchingUnions.ts | 89 - .../validate/sdl/possibleTypeExtensions.ts | 129 -- .../sdl/uniqueFieldDefinitionNames.ts | 208 -- .../validate/sdl/uniqueTypeNamesWithFields.ts | 172 -- .../apollo-federation/src/csdlDirectives.ts | 95 - packages/apollo-federation/src/directives.ts | 126 -- packages/apollo-federation/src/index.ts | 5 - .../__tests__/buildFederatedSchema.test.ts | 627 ------ .../__tests__/printComposedSdl.test.ts | 311 --- .../__tests__/printFederatedSchema.test.ts | 216 --- .../src/service/__tests__/tsconfig.json | 5 - .../src/service/buildFederatedSchema.ts | 134 -- .../apollo-federation/src/service/index.ts | 2 - .../src/service/printComposedSdl.ts | 514 ----- .../src/service/printFederatedSchema.ts | 454 ----- .../src/snapshotSerializers/astSerializer.ts | 21 - .../graphqlErrorSerializer.ts | 15 - .../src/snapshotSerializers/index.ts | 15 - .../selectionSetSerializer.ts | 13 - .../src/snapshotSerializers/typeSerializer.ts | 11 - packages/apollo-federation/src/types.ts | 124 -- packages/apollo-federation/tsconfig.json | 13 - packages/apollo-gateway/.npmignore | 6 - packages/apollo-gateway/CHANGELOG.md | 234 --- packages/apollo-gateway/LICENSE.md | 20 - packages/apollo-gateway/README.md | 27 - packages/apollo-gateway/jest.config.js | 17 - packages/apollo-gateway/package.json | 40 - packages/apollo-gateway/src/FieldSet.ts | 169 -- packages/apollo-gateway/src/QueryPlan.ts | 123 -- .../apollo-gateway/src/__tests__/.gitkeep | 0 .../src/__tests__/CucumberREADME.md | 96 - .../src/__tests__/build-query-plan.feature | 1674 ----------------- .../src/__tests__/buildQueryPlan.test.ts | 1354 ------------- .../src/__tests__/executeQueryPlan.test.ts | 724 ------- .../src/__tests__/execution-utils.ts | 113 -- .../__tests__/gateway/buildService.test.ts | 250 --- .../src/__tests__/gateway/executor.test.ts | 87 - .../__tests__/gateway/lifecycle-hooks.test.ts | 264 --- .../__tests__/gateway/queryPlanCache.test.ts | 220 --- .../src/__tests__/gateway/reporting.test.ts | 605 ------ .../integration/abstract-types.test.ts | 830 -------- .../src/__tests__/integration/aliases.test.ts | 176 -- .../src/__tests__/integration/boolean.test.ts | 277 --- .../__tests__/integration/complex-key.test.ts | 217 --- .../integration/custom-directives.test.ts | 165 -- .../integration/execution-style.test.ts | 35 - .../__tests__/integration/fragments.test.ts | 237 --- .../__tests__/integration/list-key.test.ts | 128 -- .../src/__tests__/integration/logger.test.ts | 125 -- .../integration/merge-arrays.test.ts | 34 - .../integration/multiple-key.test.ts | 328 ---- .../__tests__/integration/mutations.test.ts | 284 --- .../integration/networkRequests.test.ts | 472 ----- .../src/__tests__/integration/nockMocks.ts | 113 -- .../__tests__/integration/provides.test.ts | 77 - .../__tests__/integration/requires.test.ts | 357 ---- .../integration/single-service.test.ts | 119 -- .../src/__tests__/integration/unions.test.ts | 79 - .../__tests__/integration/value-types.test.ts | 382 ---- .../__tests__/integration/variables.test.ts | 120 -- .../loadServicesFromRemoteEndpoint.test.ts | 36 - .../src/__tests__/matchers/toCallService.ts | 105 -- .../matchers/toHaveBeenCalledBefore.ts | 40 - .../src/__tests__/matchers/toHaveFetched.ts | 81 - .../src/__tests__/matchers/toMatchAST.ts | 64 - .../src/__tests__/queryPlanCucumber.test.ts | 70 - .../apollo-gateway/src/__tests__/testSetup.ts | 4 - .../src/__tests__/tsconfig.json | 7 - packages/apollo-gateway/src/buildQueryPlan.ts | 1191 ------------ packages/apollo-gateway/src/cache.ts | 66 - .../src/datasources/LocalGraphQLDataSource.ts | 46 - .../datasources/RemoteGraphQLDataSource.ts | 234 --- .../__tests__/LocalGraphQLDataSource.test.ts | 44 - .../__tests__/RemoteGraphQLDataSource.test.ts | 544 ------ .../src/datasources/__tests__/tsconfig.json | 7 - .../apollo-gateway/src/datasources/index.ts | 3 - .../apollo-gateway/src/datasources/types.ts | 7 - .../apollo-gateway/src/executeQueryPlan.ts | 529 ------ packages/apollo-gateway/src/index.ts | 818 -------- .../src/loadServicesFromRemoteEndpoint.ts | 76 - .../src/loadServicesFromStorage.ts | 170 -- .../apollo-gateway/src/make-fetch-happen.d.ts | 59 - .../src/snapshotSerializers/astSerializer.ts | 116 -- .../src/snapshotSerializers/index.ts | 21 - .../queryPlanSerializer.ts | 152 -- .../selectionSetSerializer.ts | 13 - .../src/snapshotSerializers/typeSerializer.ts | 11 - .../apollo-gateway/src/utilities/MultiMap.ts | 11 - .../src/utilities/__tests__/deepMerge.test.ts | 77 - .../apollo-gateway/src/utilities/array.ts | 77 - .../apollo-gateway/src/utilities/deepMerge.ts | 30 - .../apollo-gateway/src/utilities/graphql.ts | 103 - .../src/utilities/predicates.ts | 8 - packages/apollo-gateway/tsconfig.json | 17 - 180 files changed, 30210 deletions(-) delete mode 100644 packages/apollo-federation-integration-testsuite/.npmignore delete mode 100644 packages/apollo-federation-integration-testsuite/package.json delete mode 100644 packages/apollo-federation-integration-testsuite/src/fixtures/accounts.ts delete mode 100644 packages/apollo-federation-integration-testsuite/src/fixtures/books.ts delete mode 100644 packages/apollo-federation-integration-testsuite/src/fixtures/documents.ts delete mode 100644 packages/apollo-federation-integration-testsuite/src/fixtures/index.ts delete mode 100644 packages/apollo-federation-integration-testsuite/src/fixtures/inventory.ts delete mode 100644 packages/apollo-federation-integration-testsuite/src/fixtures/product.ts delete mode 100644 packages/apollo-federation-integration-testsuite/src/fixtures/reviews.ts delete mode 100644 packages/apollo-federation-integration-testsuite/src/index.ts delete mode 100644 packages/apollo-federation-integration-testsuite/tsconfig.json delete mode 100644 packages/apollo-federation/.npmignore delete mode 100644 packages/apollo-federation/CHANGELOG.md delete mode 100644 packages/apollo-federation/LICENSE.md delete mode 100644 packages/apollo-federation/README.md delete mode 100644 packages/apollo-federation/jest.config.js delete mode 100644 packages/apollo-federation/package.json delete mode 100644 packages/apollo-federation/src/__tests__/tsconfig.json delete mode 100644 packages/apollo-federation/src/composition/__tests__/compose.test.ts delete mode 100644 packages/apollo-federation/src/composition/__tests__/composeAndValidate.test.ts delete mode 100644 packages/apollo-federation/src/composition/__tests__/normalize.test.ts delete mode 100644 packages/apollo-federation/src/composition/__tests__/tsconfig.json delete mode 100644 packages/apollo-federation/src/composition/__tests__/utils.test.ts delete mode 100644 packages/apollo-federation/src/composition/compose.ts delete mode 100644 packages/apollo-federation/src/composition/composeAndValidate.ts delete mode 100644 packages/apollo-federation/src/composition/index.ts delete mode 100644 packages/apollo-federation/src/composition/normalize.ts delete mode 100644 packages/apollo-federation/src/composition/rules.ts delete mode 100644 packages/apollo-federation/src/composition/types.ts delete mode 100644 packages/apollo-federation/src/composition/utils.ts delete mode 100644 packages/apollo-federation/src/composition/validate/__tests__/tsconfig.json delete mode 100644 packages/apollo-federation/src/composition/validate/index.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalUnused.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/__tests__/tsconfig.json delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesIdentical.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesInAllServices.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/externalMissingOnBase.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/externalTypeMismatch.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/externalUnused.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/index.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/keysMatchBaseService.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/providesFieldsMissingExternal.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/providesNotOnEntity.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts delete mode 100644 packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/__tests__/tsconfig.json delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumOrScalar.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumValue.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/externalUsedOnBase.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/index.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/keyFieldsMissingExternal.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/requiresUsedOnBase.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preNormalization/index.ts delete mode 100644 packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingEnums.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingUnions.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/__tests__/tsconfig.json delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueFieldDefinitionNames.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/index.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/matchingEnums.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/matchingUnions.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/possibleTypeExtensions.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts delete mode 100644 packages/apollo-federation/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts delete mode 100644 packages/apollo-federation/src/csdlDirectives.ts delete mode 100644 packages/apollo-federation/src/directives.ts delete mode 100644 packages/apollo-federation/src/index.ts delete mode 100644 packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts delete mode 100644 packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts delete mode 100644 packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts delete mode 100644 packages/apollo-federation/src/service/__tests__/tsconfig.json delete mode 100644 packages/apollo-federation/src/service/buildFederatedSchema.ts delete mode 100644 packages/apollo-federation/src/service/index.ts delete mode 100644 packages/apollo-federation/src/service/printComposedSdl.ts delete mode 100644 packages/apollo-federation/src/service/printFederatedSchema.ts delete mode 100644 packages/apollo-federation/src/snapshotSerializers/astSerializer.ts delete mode 100644 packages/apollo-federation/src/snapshotSerializers/graphqlErrorSerializer.ts delete mode 100644 packages/apollo-federation/src/snapshotSerializers/index.ts delete mode 100644 packages/apollo-federation/src/snapshotSerializers/selectionSetSerializer.ts delete mode 100644 packages/apollo-federation/src/snapshotSerializers/typeSerializer.ts delete mode 100644 packages/apollo-federation/src/types.ts delete mode 100644 packages/apollo-federation/tsconfig.json delete mode 100644 packages/apollo-gateway/.npmignore delete mode 100644 packages/apollo-gateway/CHANGELOG.md delete mode 100644 packages/apollo-gateway/LICENSE.md delete mode 100644 packages/apollo-gateway/README.md delete mode 100644 packages/apollo-gateway/jest.config.js delete mode 100644 packages/apollo-gateway/package.json delete mode 100644 packages/apollo-gateway/src/FieldSet.ts delete mode 100644 packages/apollo-gateway/src/QueryPlan.ts delete mode 100644 packages/apollo-gateway/src/__tests__/.gitkeep delete mode 100644 packages/apollo-gateway/src/__tests__/CucumberREADME.md delete mode 100644 packages/apollo-gateway/src/__tests__/build-query-plan.feature delete mode 100644 packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/execution-utils.ts delete mode 100644 packages/apollo-gateway/src/__tests__/gateway/buildService.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/gateway/executor.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/gateway/lifecycle-hooks.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/gateway/queryPlanCache.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/abstract-types.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/aliases.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/boolean.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/complex-key.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/custom-directives.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/execution-style.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/fragments.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/list-key.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/logger.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/merge-arrays.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/multiple-key.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/mutations.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/nockMocks.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/provides.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/requires.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/single-service.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/unions.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/value-types.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/integration/variables.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/loadServicesFromRemoteEndpoint.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/matchers/toCallService.ts delete mode 100644 packages/apollo-gateway/src/__tests__/matchers/toHaveBeenCalledBefore.ts delete mode 100644 packages/apollo-gateway/src/__tests__/matchers/toHaveFetched.ts delete mode 100644 packages/apollo-gateway/src/__tests__/matchers/toMatchAST.ts delete mode 100644 packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts delete mode 100644 packages/apollo-gateway/src/__tests__/testSetup.ts delete mode 100644 packages/apollo-gateway/src/__tests__/tsconfig.json delete mode 100644 packages/apollo-gateway/src/buildQueryPlan.ts delete mode 100644 packages/apollo-gateway/src/cache.ts delete mode 100644 packages/apollo-gateway/src/datasources/LocalGraphQLDataSource.ts delete mode 100644 packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts delete mode 100644 packages/apollo-gateway/src/datasources/__tests__/LocalGraphQLDataSource.test.ts delete mode 100644 packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts delete mode 100644 packages/apollo-gateway/src/datasources/__tests__/tsconfig.json delete mode 100644 packages/apollo-gateway/src/datasources/index.ts delete mode 100644 packages/apollo-gateway/src/datasources/types.ts delete mode 100644 packages/apollo-gateway/src/executeQueryPlan.ts delete mode 100644 packages/apollo-gateway/src/index.ts delete mode 100644 packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts delete mode 100644 packages/apollo-gateway/src/loadServicesFromStorage.ts delete mode 100644 packages/apollo-gateway/src/make-fetch-happen.d.ts delete mode 100644 packages/apollo-gateway/src/snapshotSerializers/astSerializer.ts delete mode 100644 packages/apollo-gateway/src/snapshotSerializers/index.ts delete mode 100644 packages/apollo-gateway/src/snapshotSerializers/queryPlanSerializer.ts delete mode 100644 packages/apollo-gateway/src/snapshotSerializers/selectionSetSerializer.ts delete mode 100644 packages/apollo-gateway/src/snapshotSerializers/typeSerializer.ts delete mode 100644 packages/apollo-gateway/src/utilities/MultiMap.ts delete mode 100644 packages/apollo-gateway/src/utilities/__tests__/deepMerge.test.ts delete mode 100644 packages/apollo-gateway/src/utilities/array.ts delete mode 100644 packages/apollo-gateway/src/utilities/deepMerge.ts delete mode 100644 packages/apollo-gateway/src/utilities/graphql.ts delete mode 100644 packages/apollo-gateway/src/utilities/predicates.ts delete mode 100644 packages/apollo-gateway/tsconfig.json diff --git a/packages/apollo-federation-integration-testsuite/.npmignore b/packages/apollo-federation-integration-testsuite/.npmignore deleted file mode 100644 index a165046d359..00000000000 --- a/packages/apollo-federation-integration-testsuite/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -* -!src/**/* -!dist/**/* -dist/**/*.test.* -!package.json -!README.md diff --git a/packages/apollo-federation-integration-testsuite/package.json b/packages/apollo-federation-integration-testsuite/package.json deleted file mode 100644 index 5f7fd597c09..00000000000 --- a/packages/apollo-federation-integration-testsuite/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "apollo-federation-integration-testsuite", - "private": true, - "version": "0.20.0", - "description": "Apollo Federation Integrations / Test Fixtures", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "repository": { - "type": "git", - "url": "https://github.com/apollographql/apollo-server", - "directory": "packages/apollo-federation-integration-testsuite" - }, - "keywords": [], - "author": "Apollo ", - "license": "MIT", - "bugs": { - "url": "https://github.com/apollographql/apollo-server/issues" - }, - "homepage": "https://github.com/apollographql/apollo-server#readme", - "engines": { - "node": ">=6" - }, - "dependencies": { - "apollo-graphql": "^0.6.0", - "graphql-tag": "^2.10.4" - } -} diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/accounts.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/accounts.ts deleted file mode 100644 index 703452e17df..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/accounts.ts +++ /dev/null @@ -1,158 +0,0 @@ -import gql from 'graphql-tag'; -import { GraphQLResolverMap } from 'apollo-graphql'; - -export const name = 'accounts'; -export const url = `https://${name}.api.com`; -export const typeDefs = gql` - directive @stream on FIELD - directive @transform(from: String!) on FIELD - - schema { - query: RootQuery - mutation: Mutation - } - - extend type RootQuery { - user(id: ID!): User - me: User - } - - type PasswordAccount @key(fields: "email") { - email: String! - } - - type SMSAccount @key(fields: "number") { - number: String - } - - union AccountType = PasswordAccount | SMSAccount - - type UserMetadata { - name: String - address: String - description: String - } - - type User @key(fields: "id") @key(fields: "username name { first last }"){ - id: ID! - name: Name - username: String - birthDate(locale: String): String - account: AccountType - metadata: [UserMetadata] - } - - type Name { - first: String - last: String - } - - type Mutation { - login(username: String!, password: String!): User - } - - extend type Library @key(fields: "id") { - id: ID! @external - name: String @external - userAccount(id: ID! = "1"): User @requires(fields: "name") - } -`; - -const users = [ - { - id: '1', - name: { - first: 'Ada', - last: 'Lovelace' - }, - birthDate: '1815-12-10', - username: '@ada', - account: { __typename: 'LibraryAccount', id: '1' }, - }, - { - id: '2', - name: { - first: 'Alan', - last: 'Turing' - }, - birthDate: '1912-06-23', - username: '@complete', - account: { __typename: 'SMSAccount', number: '8675309' }, - }, -]; - -const metadata = [ - { - id: '1', - metadata: [{ name: 'meta1', address: '1', description: '2' }], - }, - { - id: '2', - metadata: [{ name: 'meta2', address: '3', description: '4' }], - }, -]; - -const libraryUsers: { [name: string]: string[] } = { - 'NYC Public Library': ['1', '2'], -}; - -export const resolvers: GraphQLResolverMap = { - RootQuery: { - user(_, args) { - return { id: args.id }; - }, - - me() { - return { id: '1' }; - }, - }, - User: { - __resolveObject(object) { - // Nested key example for @key(fields: "username name { first last }") - if (object.username && object.name.first && object.name.last) { - users.find(user => user.username === object.username); - } - - return users.find(user => user.id === object.id); - }, - birthDate(user, args) { - return args.locale - ? new Date(user.birthDate).toLocaleDateString(args.locale, { - timeZone: 'Asia/Samarkand', // UTC + 5 - }) - : user.birthDate; - }, - metadata(object) { - const metaIndex = metadata.findIndex(m => m.id === object.id); - return metadata[metaIndex].metadata.map(obj => ({ name: obj.name })); - }, - }, - UserMetadata: { - address(object) { - const metaIndex = metadata.findIndex(m => - m.metadata.find(o => o.name === object.name), - ); - return metadata[metaIndex].metadata[0].address; - }, - description(object) { - const metaIndex = metadata.findIndex(m => - m.metadata.find(o => o.name === object.name), - ); - return metadata[metaIndex].metadata[0].description; - }, - }, - Library: { - userAccount({ name }, { id: userId }) { - const libraryUserIds = libraryUsers[name]; - return libraryUserIds && - libraryUserIds.find((id: string) => id === userId) - ? { id: userId } - : null; - }, - }, - Mutation: { - login(_, args) { - return users.find(user => user.username === args.username); - }, - }, -}; diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/books.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/books.ts deleted file mode 100644 index 4565214a21c..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/books.ts +++ /dev/null @@ -1,129 +0,0 @@ -import gql from 'graphql-tag'; -import { GraphQLResolverMap } from 'apollo-graphql'; - -export const name = 'books'; -export const url = `https://${name}.api.com`; -export const typeDefs = gql` - directive @stream on FIELD - directive @transform(from: String!) on FIELD - - extend type Query { - book(isbn: String!): Book - books: [Book] - library(id: ID!): Library - } - - type Library @key(fields: "id") { - id: ID! - name: String - } - - # FIXME: turn back on when unions are supported in composition - # type LibraryAccount @key(fields: "id") { - # id: ID! - # library: Library - # } - - # extend union AccountType = LibraryAccount - - type Book @key(fields: "isbn") { - isbn: String! - title: String - year: Int - similarBooks: [Book]! - metadata: [MetadataOrError] - } - - # Value type - type KeyValue { - key: String! - value: String! - } - - # Value type - type Error { - code: Int - message: String - } - - # Value type - union MetadataOrError = KeyValue | Error -`; - -const libraries = [{ id: '1', name: 'NYC Public Library' }]; -const books = [ - { - isbn: '0262510871', - title: 'Structure and Interpretation of Computer Programs', - year: 1996, - metadata: [{ key: 'Condition', value: 'excellent' }], - }, - { - isbn: '0136291554', - title: 'Object Oriented Software Construction', - year: 1997, - metadata: [ - { key: 'Condition', value: 'used' }, - { code: '401', message: 'Unauthorized' }, - ], - }, - { - isbn: '0201633612', - title: 'Design Patterns', - year: 1995, - similarBooks: ['0201633612', '0136291554'], - metadata: [{ key: 'Condition', value: 'like new' }], - }, - { - isbn: '1234567890', - title: 'The Year Was Null', - year: null, - }, - { - isbn: '404404404', - title: '', - year: 404, - }, - { - isbn: '0987654321', - title: 'No Books Like This Book!', - year: 2019, - similarBooks: ['', null], - }, -]; - -export const resolvers: GraphQLResolverMap = { - Book: { - __resolveObject(object) { - return books.find(book => book.isbn === object.isbn); - }, - similarBooks(object) { - return object.similarBooks - ? object.similarBooks - .map((isbn: string) => books.find(book => book.isbn === isbn)) - .filter(Boolean) - : []; - }, - }, - Library: { - __resolveReference(object) { - return libraries.find(library => library.id === object.id); - }, - }, - Query: { - book(_, args) { - return { isbn: args.isbn }; - }, - books() { - return books; - }, - library(_, { id }) { - return libraries.find(library => library.id === id); - }, - }, - MetadataOrError: { - __resolveType(object) { - return 'key' in object ? 'KeyValue' : 'Error'; - }, - }, -}; diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/documents.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/documents.ts deleted file mode 100644 index a237449ec8e..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/documents.ts +++ /dev/null @@ -1,35 +0,0 @@ -import gql from 'graphql-tag'; - -export const name = 'documents'; -export const url = `https://${name}.api.com`; -export const typeDefs = gql` - directive @stream on FIELD - directive @transform(from: String!) on FIELD - - extend type Query { - body: Body! - } - - union Body = Image | Text - - type Image { - name: String! - # Same as option below but the type is different - attributes: ImageAttributes! - } - - type Text { - name: String! - # Same as option above but the type is different - attributes: TextAttributes! - } - - type ImageAttributes { - url: String! - } - - type TextAttributes { - bold: Boolean - text: String - } -`; diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/index.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/index.ts deleted file mode 100644 index e051f45b76a..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as accounts from './accounts'; -import * as books from './books'; -import * as documents from './documents'; -import * as inventory from './inventory'; -import * as product from './product'; -import * as reviews from './reviews'; - -export { - accounts, - books, - documents, - inventory, - product, - reviews, -}; - -export const fixtures = [ - accounts, - books, - documents, - inventory, - product, - reviews, -]; - -export const fixtureNames = [ - accounts.name, - product.name, - inventory.name, - reviews.name, - books.name, - documents.name, -]; diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/inventory.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/inventory.ts deleted file mode 100644 index 603e1bc175a..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/inventory.ts +++ /dev/null @@ -1,62 +0,0 @@ -import gql from 'graphql-tag'; -import { GraphQLResolverMap } from 'apollo-graphql'; - -export const name = 'inventory'; -export const url = `https://${name}.api.com`; -export const typeDefs = gql` - directive @stream on FIELD - directive @transform(from: String!) on FIELD - - extend interface Product { - inStock: Boolean - } - - extend type Furniture implements Product @key(fields: "sku") { - sku: String! @external - inStock: Boolean - isHeavy: Boolean - } - - extend type Book implements Product @key(fields: "isbn") { - isbn: String! @external - inStock: Boolean - isCheckedOut: Boolean - } - - extend type UserMetadata { - description: String @external - } - - extend type User @key(fields: "id") { - id: ID! @external - metadata: [UserMetadata] @external - goodDescription: Boolean @requires(fields: "metadata { description }") - } -`; - -const inventory = [ - { sku: 'TABLE1', inStock: true, isHeavy: false }, - { sku: 'COUCH1', inStock: false, isHeavy: true }, - { sku: 'CHAIR1', inStock: true, isHeavy: false }, - { isbn: '0262510871', inStock: true, isCheckedOut: true }, - { isbn: '0136291554', inStock: false, isCheckedOut: false }, - { isbn: '0201633612', inStock: true, isCheckedOut: false }, -]; - -export const resolvers: GraphQLResolverMap = { - Furniture: { - __resolveReference(object) { - return inventory.find(product => product.sku === object.sku); - }, - }, - Book: { - __resolveReference(object) { - return inventory.find(product => product.isbn === object.isbn); - }, - }, - User: { - goodDescription(object) { - return object.metadata[0].description === '2'; - }, - }, -}; diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/product.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/product.ts deleted file mode 100644 index 344222eb46f..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/product.ts +++ /dev/null @@ -1,247 +0,0 @@ -import gql from 'graphql-tag'; -import { GraphQLResolverMap } from 'apollo-graphql'; - -export const name = 'product'; -export const url = `https://${name}.api.com`; -export const typeDefs = gql` - directive @stream on FIELD - directive @transform(from: String!) on FIELD - - extend type Query { - product(upc: String!): Product - vehicle(id: String!): Vehicle - topProducts(first: Int = 5): [Product] - topCars(first: Int = 5): [Car] - } - - type Ikea { - asile: Int - } - - type Amazon { - referrer: String - } - - union Brand = Ikea | Amazon - - interface Product { - upc: String! - sku: String! - name: String - price: String - details: ProductDetails - } - - interface ProductDetails { - country: String - } - - type ProductDetailsFurniture implements ProductDetails { - country: String - color: String - } - - type ProductDetailsBook implements ProductDetails { - country: String - pages: Int - } - - type Furniture implements Product @key(fields: "upc") @key(fields: "sku") { - upc: String! - sku: String! - name: String - price: String - brand: Brand - metadata: [MetadataOrError] - details: ProductDetailsFurniture - } - - extend type Book implements Product @key(fields: "isbn") { - isbn: String! @external - title: String @external - year: Int @external - upc: String! - sku: String! - name(delimeter: String = " "): String @requires(fields: "title year") - price: String - details: ProductDetailsBook - } - - interface Vehicle { - id: String! - description: String - price: String - } - - type Car implements Vehicle @key(fields: "id") { - id: String! - description: String - price: String - } - - type Van implements Vehicle @key(fields: "id") { - id: String! - description: String - price: String - } - - union Thing = Car | Ikea - - extend type User @key(fields: "id") { - id: ID! @external - vehicle: Vehicle - thing: Thing - } - - # Value type - type KeyValue { - key: String! - value: String! - } - - # Value type - type Error { - code: Int - message: String - } - - # Value type - union MetadataOrError = KeyValue | Error -`; - -const products = [ - { - __typename: 'Furniture', - upc: '1', - sku: 'TABLE1', - name: 'Table', - price: 899, - brand: { - __typename: 'Ikea', - asile: 10, - }, - metadata: [{ key: 'Condition', value: 'excellent' }], - }, - { - __typename: 'Furniture', - upc: '2', - sku: 'COUCH1', - name: 'Couch', - price: 1299, - brand: { - __typename: 'Amazon', - referrer: 'https://canopy.co', - }, - metadata: [{ key: 'Condition', value: 'used' }], - }, - { - __typename: 'Furniture', - upc: '3', - sku: 'CHAIR1', - name: 'Chair', - price: 54, - brand: { - __typename: 'Ikea', - asile: 10, - }, - metadata: [{ key: 'Condition', value: 'like new' }], - }, - { __typename: 'Book', isbn: '0262510871', price: 39 }, - { __typename: 'Book', isbn: '0136291554', price: 29 }, - { __typename: 'Book', isbn: '0201633612', price: 49 }, - { __typename: 'Book', isbn: '1234567890', price: 59 }, - { __typename: 'Book', isbn: '404404404', price: 0 }, - { __typename: 'Book', isbn: '0987654321', price: 29 }, -]; - -const vehicles = [ - { - __typename: 'Car', - id: '1', - description: 'Humble Toyota', - price: 9990, - }, - { - __typename: 'Car', - id: '2', - description: 'Awesome Tesla', - price: 12990, - }, - { - __typename: 'Van', - id: '3', - description: 'Just a van...', - price: 15990, - }, -]; - -export const resolvers: GraphQLResolverMap = { - Furniture: { - __resolveReference(object) { - return products.find( - product => product.upc === object.upc || product.sku === object.sku, - ); - }, - }, - Book: { - __resolveReference(object) { - if (object.isbn) { - const fetchedObject = products.find( - product => product.isbn === object.isbn, - ); - if (fetchedObject) { - return { ...object, ...fetchedObject }; - } - } - return object; - }, - name(object, { delimeter }) { - return `${object.title}${delimeter}(${object.year})`; - }, - upc(object) { - return object.isbn; - }, - sku(object) { - return object.isbn; - }, - }, - Car: { - __resolveReference(object) { - return vehicles.find(vehicles => vehicles.id === object.id); - }, - }, - Van: { - __resolveReference(object) { - return vehicles.find(vehicles => vehicles.id === object.id); - }, - }, - Thing: { - __resolveType(object) { - return 'id' in object ? 'Car' : 'Ikea'; - }, - }, - User: { - vehicle(user) { - return vehicles.find(vehicles => vehicles.id === user.id); - }, - thing(user) { - return vehicles.find(vehicles => vehicles.id === user.id); - }, - }, - Query: { - product(_, args) { - return products.find(product => product.upc === args.upc); - }, - vehicle(_, args) { - return vehicles.find(vehicles => vehicles.id === args.id); - }, - topProducts(_, args) { - return products.slice(0, args.first); - }, - }, - MetadataOrError: { - __resolveType(object) { - return 'key' in object ? 'KeyValue' : 'Error'; - }, - }, -}; diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/reviews.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/reviews.ts deleted file mode 100644 index 7660773bc49..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/reviews.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { GraphQLResolverMap } from 'apollo-graphql'; -import gql from 'graphql-tag'; - -export const name = 'reviews'; -export const url = `https://${name}.api.com`; -export const typeDefs = gql` - directive @stream on FIELD - directive @transform(from: String!) on FIELD - - extend type Query { - topReviews(first: Int = 5): [Review] - } - - type Review @key(fields: "id") { - id: ID! - body(format: Boolean = false): String - author: User @provides(fields: "username") - product: Product - metadata: [MetadataOrError] - } - - input UpdateReviewInput { - id: ID! - body: String - } - - extend type UserMetadata { - address: String @external - } - - extend type User @key(fields: "id") { - id: ID! @external - username: String @external - reviews: [Review] - numberOfReviews: Int! - metadata: [UserMetadata] @external - goodAddress: Boolean @requires(fields: "metadata { address }") - } - - extend interface Product { - reviews: [Review] - } - - extend type Furniture implements Product @key(fields: "upc") { - upc: String! @external - reviews: [Review] - } - - extend type Book implements Product @key(fields: "isbn") { - isbn: String! @external - reviews: [Review] - similarBooks: [Book]! @external - relatedReviews: [Review!]! @requires(fields: "similarBooks { isbn }") - } - - extend interface Vehicle { - retailPrice: String - } - - extend type Car implements Vehicle @key(fields: "id") { - id: String! @external - price: String @external - retailPrice: String @requires(fields: "price") - } - - extend type Van implements Vehicle @key(fields: "id") { - id: String! @external - price: String @external - retailPrice: String @requires(fields: "price") - } - - extend type Mutation { - reviewProduct(upc: String!, body: String!): Product - updateReview(review: UpdateReviewInput!): Review - deleteReview(id: ID!): Boolean - } - - # Value type - type KeyValue { - key: String! - value: String! - } - - # Value type - type Error { - code: Int - message: String - } - - # Value type - union MetadataOrError = KeyValue | Error -`; - -const usernames = [ - { id: '1', username: '@ada' }, - { id: '2', username: '@complete' }, -]; -const reviews = [ - { - id: '1', - authorID: '1', - product: { __typename: 'Furniture', upc: '1' }, - body: 'Love it!', - metadata: [{ code: 418, message: "I'm a teapot" }], - }, - { - id: '2', - authorID: '1', - product: { __typename: 'Furniture', upc: '2' }, - body: 'Too expensive.', - }, - { - id: '3', - authorID: '2', - product: { __typename: 'Furniture', upc: '3' }, - body: 'Could be better.', - }, - { - id: '4', - authorID: '2', - product: { __typename: 'Furniture', upc: '1' }, - body: 'Prefer something else.', - }, - { - id: '4', - authorID: '2', - product: { __typename: 'Book', isbn: '0262510871' }, - body: 'Wish I had read this before.', - }, - { - id: '5', - authorID: '2', - product: { __typename: 'Book', isbn: '0136291554' }, - body: 'A bit outdated.', - metadata: [{ key: 'likes', value: '5' }], - }, - { - id: '6', - authorID: '1', - product: { __typename: 'Book', isbn: '0201633612' }, - body: 'A classic.', - }, -]; - -export const resolvers: GraphQLResolverMap = { - Query: { - review(_, args) { - return { id: args.id }; - }, - topReviews(_, args) { - return reviews.slice(0, args.first); - }, - }, - Mutation: { - reviewProduct(_, { upc, body }) { - const id = `${Number(reviews[reviews.length - 1].id) + 1}`; - reviews.push({ - id, - authorID: '1', - product: { __typename: 'Furniture', upc }, - body, - }); - return { upc, __typename: 'Furniture' }; - }, - updateReview(_, { review: { id }, review: updatedReview }) { - let review = reviews.find(review => review.id === id); - - if (!review) { - return null; - } - - review = { - ...review, - ...updatedReview, - }; - - return review; - }, - deleteReview(_, { id }) { - const deleted = reviews.splice( - reviews.findIndex(review => review.id === id), - 1, - ); - return Boolean(deleted); - }, - }, - Review: { - author(review) { - return { __typename: 'User', id: review.authorID }; - }, - }, - User: { - reviews(user) { - return reviews.filter(review => review.authorID === user.id); - }, - numberOfReviews(user) { - return reviews.filter(review => review.authorID === user.id).length; - }, - username(user) { - const found = usernames.find(username => username.id === user.id); - return found ? found.username : null; - }, - goodAddress(object) { - return object.metadata[0].address === '1'; - }, - }, - Furniture: { - reviews(product) { - return reviews.filter(review => review.product.upc === product.upc); - }, - }, - Book: { - reviews(product) { - return reviews.filter(review => review.product.isbn === product.isbn); - }, - relatedReviews(book) { - return book.similarBooks - ? book.similarBooks - .map(({ isbn }: any) => - reviews.filter(review => review.product.isbn === isbn), - ) - .flat() - : []; - }, - }, - Car: { - retailPrice(car) { - return car.price; - }, - }, - Van: { - retailPrice(van) { - return van.price; - }, - }, - MetadataOrError: { - __resolveType(object) { - return 'key' in object ? 'KeyValue' : 'Error'; - }, - }, -}; diff --git a/packages/apollo-federation-integration-testsuite/src/index.ts b/packages/apollo-federation-integration-testsuite/src/index.ts deleted file mode 100644 index 995b5bc6e1f..00000000000 --- a/packages/apollo-federation-integration-testsuite/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './fixtures'; diff --git a/packages/apollo-federation-integration-testsuite/tsconfig.json b/packages/apollo-federation-integration-testsuite/tsconfig.json deleted file mode 100644 index 7b38112eee6..00000000000 --- a/packages/apollo-federation-integration-testsuite/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist", - "noImplicitAny": false, - "strictNullChecks": false, - "types": ["node", "jest"] - }, - "include": ["src/**/*"], - "exclude": ["**/__tests__", "**/__mocks__"], - "references": [] -} diff --git a/packages/apollo-federation/.npmignore b/packages/apollo-federation/.npmignore deleted file mode 100644 index a165046d359..00000000000 --- a/packages/apollo-federation/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -* -!src/**/* -!dist/**/* -dist/**/*.test.* -!package.json -!README.md diff --git a/packages/apollo-federation/CHANGELOG.md b/packages/apollo-federation/CHANGELOG.md deleted file mode 100644 index 9818d4ab042..00000000000 --- a/packages/apollo-federation/CHANGELOG.md +++ /dev/null @@ -1,177 +0,0 @@ -# CHANGELOG for `@apollo/federation` - -## vNEXT - -> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. - -- _Nothing yet! Stay tuned!_ - -## v0.20.0 - -- __FIX__: CSDL complex `@key`s shouldn't result in an unparseable document [PR #4490](https://github.com/apollographql/apollo-server/pull/4490) -- __FIX__: Value type validations - restrict unions, scalars, enums [PR #4496](https://github.com/apollographql/apollo-server/pull/4496) -- __FIX__: Composition - aggregate interfaces for types and interfaces in composed schema [PR #4497](https://github.com/apollographql/apollo-server/pull/4497) -- __FIX__: Create new `@key` validations to prevent invalid compositions [PR #4498](https://github.com/apollographql/apollo-server/pull/4498) -- CSDL: make `fields` directive args parseable [PR #4489](https://github.com/apollographql/apollo-server/pull/4489) - -## v0.19.1 - -- Include new directive definitions in CSDL [PR #4452](https://github.com/apollographql/apollo-server/pull/4452) - -## v0.19.0 - -- New federation composition format. Capture federation metadata in SDL [PR #4405](https://github.com/apollographql/apollo-server/pull/4405) - -## v0.18.1 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.18.0 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.17.0 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.16.11 - -- Reinstate typings for `make-fetch-happen` at the `apollo-gateway` project level (and now, additionally, `apollo-server-plugin-operation-registry`) [PR #4333](https://github.com/apollographql/apollo-server/pull/4333) - -## 0.16.10 - -- The default branch of the repository has been changed to `main`. As this changed a number of references in the repository's `package.json` and `README.md` files (e.g., for badges, links, etc.), this necessitates a release to publish those changes to npm. [PR #4302](https://github.com/apollographql/apollo-server/pull/4302) -- __BREAKING__: Move federation metadata from custom objects on schema nodes over to the `extensions` field on schema nodes which are intended for metadata. This is a breaking change because it narrows the `graphql` peer dependency from `^14.0.2` to `^14.5.0` which is when [`extensions` were introduced](https://github.com/graphql/graphql-js/pull/2097) for all Type System objects. [PR #4302](https://github.com/apollographql/apollo-server/pull/4313) - -## 0.16.9 - -- Handle `@external` validation edge case for interface implementors [#4284](https://github.com/apollographql/apollo-server/pull/4284) - -## 0.16.7 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.16.6 - -- In-house `Maybe` type which was previously imported from `graphql` and has been moved in `v15.1.0`. [#4230](https://github.com/apollographql/apollo-server/pull/4230) -- Remove remaining common primitives from SDL during composition. This is a follow up to [#4209](https://github.com/apollographql/apollo-server/pull/4209), and additionally removes directives which are included in a schema by default (`@skip`, `@include`, `@deprecated`, and `@specifiedBy`) [#4228](https://github.com/apollographql/apollo-server/pull/4209) - -## v0.16.5 - -- Remove federation primitives from SDL during composition. This allows for services to report their *full* SDL from the `{ _service { sdl } }` query as opposed to the previously limited SDL without federation definitions. [#4209](https://github.com/apollographql/apollo-server/pull/4209) - -## v0.16.4 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.16.3 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.16.2 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.16.1 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.16.0 - -- No changes. This package was major versioned to maintain lockstep versioning with @apollo/gateway. - -## v0.15.1 - -- Export `defaultRootOperationNameLookup` and `normalizeTypeDefs`; needed by `@apollo/gateway` to normalize root operation types when reporting to Apollo Graph Manager. [#4071](https://github.com/apollographql/apollo-server/pull/4071) - -## v0.15.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/e37384a49b2bf474eed0de3e9f4a1bebaeee64c7) - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.14.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/b898396e9fcd3b9092b168f9aac8466ca186fa6b) - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.14.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/71a3863f59f4ab2c9052c316479d94c6708c4309) - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.13.2 - -- Only changes in the similarly versioned `@apollo/gateway` package. - -## v0.12.1 - -- Fix `v0.12.0` regression: Preserve the `@deprecated` type-system directive as a special case when removing type system directives during composition, resolving an unintentional breaking change introduced by [#3736](https://github.com/apollographql/apollo-server/pull/3736). [#3792](https://github.com/apollographql/apollo-server/pull/3792) - -## v0.12.0 - -- Strip all Type System Directives during composition [#3736](https://github.com/apollographql/apollo-server/pull/3736) -- Prepare for changes in upcoming `graphql@15` release. [#3712](https://github.com/apollographql/apollo-server/pull/3712) - -## v0.11.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/2a4c654986a158aaccf947ee56a4bfc48a3173c7) - -- Ignore TypeSystemDirectiveLocations during composition [#3536](https://github.com/apollographql/apollo-server/pull/3536) - -## v0.11.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/93002737d53dd9a50b473ab9cef14849b3e539aa) - -- Begin supporting executable directives in federation [#3464](https://github.com/apollographql/apollo-server/pull/3464) - -## v0.10.3 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/3cdde1b7a71ace6411fbacf82a1a61bf737444a6) - -- Remove `apollo-env` dependency to eliminate circular dependency between the two packages. This circular dependency makes the tooling repo unpublishable when `apollo-env` requires a version bump. [#3463](https://github.com/apollographql/apollo-server/pull/3463) - -## v0.10.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/aa200ce24b834320fc79d2605dac340b37d3e434) - -- Use reference-equality when omitting validation rules during composition. [#3338](https://github.com/apollographql/apollo-server/pull/3338) - -## v0.10.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/6100fb5e0797cd1f578ded7cb77b60fac47e58e3) - -- Remove federation directives from composed schema [#3272](https://github.com/apollographql/apollo-server/pull/3272) -- Do not remove Query/Mutation/Subscription types when schema is included if schema references those types [#3260](https://github.com/apollographql/apollo-server/pull/3260) - -## v0.9.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/029c8dca3af812ee70589cdb6de749df3d2843d8) - -- Fix value type behavior within composition and execution [#3182](https://github.com/apollographql/apollo-server/pull/2922) - -## v0.6.8 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/5974b2ce405a06bc331230400b9073f6381738d3) - -- Support __typenames if defined by an incoming operation [#2922](https://github.com/apollographql/apollo-server/pull/2922) - -## v0.6.7 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/2ea5887acc43461a5539071f4981a5f70e0d0652) - -- Fix bug in externalUnused validation [#2919](https://github.com/apollographql/apollo-server/pull/2919) - -## v0.6.6 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/183de5f112324def375a45c239955e1bf1608fae) - -- Allow specified directives during validation (@deprecated) [#2823](https://github.com/apollographql/apollo-server/pull/2823) - -## v0.6.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/1209839c01b4cac1eb23f42c747296dd9507a8ac) - -- Normalize SDL in a normalization step before validation [#2771](https://github.com/apollographql/apollo-server/pull/2771) diff --git a/packages/apollo-federation/LICENSE.md b/packages/apollo-federation/LICENSE.md deleted file mode 100644 index dce88b77022..00000000000 --- a/packages/apollo-federation/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License - -Copyright (c) 2019 Meteor Development Group, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/apollo-federation/README.md b/packages/apollo-federation/README.md deleted file mode 100644 index 7980e3bdb4c..00000000000 --- a/packages/apollo-federation/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# `Apollo Federation Utilities` - -This package provides utilities for creating GraphQL microservices, which can be combined into a single endpoint through tools like [Apollo Gateway](https://github.com/apollographql/apollo-server/tree/main/packages/apollo-gateway). - -For complete documentation, see the [Apollo Federation API reference](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/). - -## Usage - -```js -const { ApolloServer, gql } = require("apollo-server"); -const { buildFederatedSchema } = require("@apollo/federation"); - -const typeDefs = gql` - type Query { - me: User - } - - type User @key(fields: "id") { - id: ID! - username: String - } -`; - -const resolvers = { - Query: { - me() { - return { id: "1", username: "@ava" } - } - }, - User: { - __resolveReference(user, { fetchUserById }){ - return fetchUserById(user.id) - } - } -}; - -const server = new ApolloServer({ - schema: buildFederatedSchema([{ typeDefs, resolvers }]) -}); -``` diff --git a/packages/apollo-federation/jest.config.js b/packages/apollo-federation/jest.config.js deleted file mode 100644 index a54e180ea6d..00000000000 --- a/packages/apollo-federation/jest.config.js +++ /dev/null @@ -1,19 +0,0 @@ -const config = require('../../jest.config.base'); - -const NODE_MAJOR_VERSION = parseInt( - process.versions.node.split('.', 1)[0], - 10 -); - -const additionalConfig = { - setupFiles: [ - 'core-js/features/array/flat', - 'core-js/features/array/flat-map', - ], - testPathIgnorePatterns: [ - ...config.testPathIgnorePatterns, - ...NODE_MAJOR_VERSION === 6 ? [""] : [] - ] -}; - -module.exports = Object.assign(Object.create(null), config, additionalConfig); diff --git a/packages/apollo-federation/package.json b/packages/apollo-federation/package.json deleted file mode 100644 index b64b8b9a613..00000000000 --- a/packages/apollo-federation/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@apollo/federation", - "version": "0.20.0", - "description": "Apollo Federation Utilities", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "keywords": [], - "author": "Apollo ", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "publishConfig": { - "access": "public" - }, - "dependencies": { - "apollo-graphql": "^0.6.0", - "apollo-server-env": "file:../apollo-server-env", - "core-js": "^3.4.0", - "lodash.xorby": "^4.7.0" - }, - "peerDependencies": { - "graphql": "^14.5.0 || ^15.0.0" - } -} diff --git a/packages/apollo-federation/src/__tests__/tsconfig.json b/packages/apollo-federation/src/__tests__/tsconfig.json deleted file mode 100644 index d7cd9b716cc..00000000000 --- a/packages/apollo-federation/src/__tests__/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [ - { "path": "../../" }, - ] -} diff --git a/packages/apollo-federation/src/composition/__tests__/compose.test.ts b/packages/apollo-federation/src/composition/__tests__/compose.test.ts deleted file mode 100644 index 5a58f0b20a8..00000000000 --- a/packages/apollo-federation/src/composition/__tests__/compose.test.ts +++ /dev/null @@ -1,1363 +0,0 @@ -import { - GraphQLObjectType, - isSpecifiedDirective, - GraphQLDirective, -} from 'graphql'; -import gql from 'graphql-tag'; -import { composeServices } from '../compose'; -import { - astSerializer, - typeSerializer, - selectionSetSerializer, -} from '../../snapshotSerializers'; -import { normalizeTypeDefs } from '../normalize'; -import { getFederationMetadata } from '../utils'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(typeSerializer); -expect.addSnapshotSerializer(selectionSetSerializer); - -describe('composeServices', () => { - it('should include types from different services', () => { - const serviceA = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type User { - name: String - email: String! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('User')).toMatchInlineSnapshot(` - type User { - name: String - email: String! - } - `); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - } - `); - - const product = schema.getType('Product') as GraphQLObjectType; - const user = schema.getType('User') as GraphQLObjectType; - - expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); - expect(getFederationMetadata(user).serviceName).toEqual('serviceB'); - }); - - it("doesn't leave federation directives in the final schema", () => { - const serviceA = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const { schema } = composeServices([serviceA]); - - const directives = schema.getDirectives(); - expect(directives.every(isSpecifiedDirective)); - }); - - describe('basic type extensions', () => { - it('works when extension service is second', () => { - const serviceA = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - price: Int! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - price: Int! - } - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); - expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( - 'serviceB', - ); - }); - - it('works when extension service is first', () => { - const serviceA = { - typeDefs: gql` - extend type Product { - price: Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceB', - }; - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - price: Int! - } - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(getFederationMetadata(product).serviceName).toEqual('serviceB'); - expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( - 'serviceA', - ); - }); - - it('works with multiple extensions on the same type', () => { - const serviceA = { - typeDefs: gql` - extend type Product { - price: Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - extend type Product { - color: String! - } - `, - name: 'serviceC', - }; - - const { schema, errors } = composeServices([ - serviceA, - serviceB, - serviceC, - ]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - price: Int! - color: String! - } - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(getFederationMetadata(product).serviceName).toEqual('serviceB'); - expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( - 'serviceA', - ); - expect(getFederationMetadata(product.getFields()['color']).serviceName).toEqual( - 'serviceC', - ); - }); - - it('allows extensions to overwrite other extension fields', () => { - const serviceA = { - typeDefs: gql` - extend type Product { - price: Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - extend type Product { - price: Float! - color: String! - } - `, - name: 'serviceC', - }; - - const { schema, errors } = composeServices([ - serviceA, - serviceB, - serviceC, - ]); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Field "Product.price" can only be defined once.], - ] - `); - expect(schema).toBeDefined(); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(product).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - price: Float! - color: String! - } - `); - - expect(getFederationMetadata(product).serviceName).toEqual('serviceB'); - expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( - 'serviceC', - ); - }); - - it('preserves arguments for fields', () => { - const serviceA = { - typeDefs: gql` - enum Curr { - USD - GBP - } - - extend type Product { - price(currency: Curr!): Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product { - sku: String! - name(type: String): String! - } - `, - name: 'serviceB', - }; - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` - type Product { - sku: String! - name(type: String): String! - price(currency: Curr!): Int! - } - `); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(product.getFields()['price'].args[0].name).toEqual('currency'); - }); - - // This is a limitation of extendSchema currently (this is currently a broken test to demonstrate) - it.skip('overwrites field on extension by base type when base type comes second', () => { - const serviceA = { - typeDefs: gql` - extend type Product { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - const serviceB = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(schema).toBeDefined(); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Field "Product.sku" already exists in the schema. It cannot also be defined in this type extension.], - [GraphQLError: Field "Product.name" already exists in the schema. It cannot also be defined in this type extension.], - ] - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(product).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - } - `); - expect(getFederationMetadata(product.getFields()['sku']).serviceName).toEqual( - 'serviceB', - ); - expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual( - 'serviceB', - ); - }); - - describe('collisions & error handling', () => { - it('handles collisions on type extensions as expected', () => { - const serviceA = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - name: String! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(schema).toBeDefined(); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: [serviceA] Product.name -> Field "Product.name" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], - ] - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(product).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - } - `); - expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual( - 'serviceB', - ); - }); - - it('reports multiple errors correctly', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - } - - type Product { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! - name: String! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(schema).toBeDefined(); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: [serviceA] Product.sku -> Field "Product.sku" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], - [GraphQLError: [serviceA] Product.name -> Field "Product.name" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], - ] - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(product).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - } - `); - expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual( - 'serviceB', - ); - }); - - it('handles collisions of base types as expected (newest takes precedence)', () => { - const serviceA = { - typeDefs: gql` - type Product { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product { - id: ID! - name: String! - price: Int! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(schema).toBeDefined(); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Field "Product.name" can only be defined once.], - [GraphQLError: There can be only one type named "Product".], - ] - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(product).toMatchInlineSnapshot(` - type Product { - id: ID! - name: String! - price: Int! - } - `); - }); - }); - }); - - // Maybe just test conflicts in types - // it("interfaces, unions", () => {}); - - // TODO: _allow_ enum and input extensions, but don't add serviceName - describe('input and enum type extensions', () => { - it('extends input types', () => { - const serviceA = { - typeDefs: gql` - input ProductInput { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend input ProductInput { - color: String! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(schema).toBeDefined(); - expect(errors).toMatchInlineSnapshot(`Array []`); - }); - - it('extends enum types', () => { - const serviceA = { - typeDefs: gql` - enum ProductCategory { - BED - BATH - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend enum ProductCategory { - BEYOND - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(schema).toBeDefined(); - expect(errors).toMatchInlineSnapshot(`Array []`); - }); - }); - - describe('interfaces', () => { - // TODO: should there be a validation warning of some sort for this? - it('allows overwriting a type that implements an interface improperly', () => { - const serviceA = { - typeDefs: gql` - interface Item { - id: ID! - } - - type Product implements Item { - id: ID! - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - id: String! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: [serviceA] Product.id -> Field "Product.id" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], - ] - `); - expect(schema).toBeDefined(); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` - type Product implements Item { - id: String! - sku: String! - name: String! - } - `); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); - expect(getFederationMetadata(product.getFields()['id']).serviceName).toEqual( - 'serviceB', - ); - }); - }); - - describe('root type extensions', () => { - it('allows extension of the Query type with no base type definition', () => { - const serviceA = { - typeDefs: gql` - extend type Query { - products: [ID!] - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Query { - people: [ID!] - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` - type Query { - products: [ID!] - people: [ID!] - } - `); - - const query = schema.getQueryType(); - - expect(getFederationMetadata(query).serviceName).toBeUndefined(); - }); - - it('treats root Query type definition as an extension, not base definitions', () => { - const serviceA = { - typeDefs: gql` - type Query { - products: [ID!] - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Query { - people: [ID!] - } - `, - name: 'serviceB', - }; - - const normalizedServices = [serviceA, serviceB].map( - ({ name, typeDefs }) => ({ - name, - typeDefs: normalizeTypeDefs(typeDefs), - }), - ); - const { schema, errors } = composeServices(normalizedServices); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('Query')).toMatchInlineSnapshot(` - type Query { - products: [ID!] - people: [ID!] - } - `); - - const query = schema.getType('Query') as GraphQLObjectType; - - expect(getFederationMetadata(query).serviceName).toBeUndefined(); - }); - - it('allows extension of the Mutation type with no base type definition', () => { - const serviceA = { - typeDefs: gql` - extend type Mutation { - login(credentials: Credentials!): String - } - - input Credentials { - username: String! - password: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Mutation { - logout(username: String!): Boolean - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('Mutation')).toMatchInlineSnapshot(` - type Mutation { - login(credentials: Credentials!): String - logout(username: String!): Boolean - } - `); - }); - - it('treats root Mutations type definition as an extension, not base definitions', () => { - const serviceA = { - typeDefs: gql` - type Mutation { - login(credentials: Credentials!): String - } - - input Credentials { - username: String! - password: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Mutation { - logout(username: String!): Boolean - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - expect(schema).toBeDefined(); - - expect(schema.getType('Mutation')).toMatchInlineSnapshot(` - type Mutation { - login(credentials: Credentials!): String - logout(username: String!): Boolean - } - `); - }); - - // TODO: not sure what to do here. Haven't looked into it yet :) - it.skip('works with custom root types', () => {}); - }); - - describe('federation directives', () => { - // Directives - allow schema (federation) directives - describe('@external', () => { - it('adds externals map from service to externals for @external fields', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA--FOUND', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB--MISSING', - }; - - const serviceC = { - typeDefs: gql` - extend type Product { - sku: String! @external - upc: String! @external - weight: Int! @requires(fields: "sku upc") - } - `, - name: 'serviceC--found', - }; - - const { schema, errors } = composeServices([ - serviceA, - serviceC, - serviceB, - ]); - - expect(errors).toHaveLength(0); - - const product = schema.getType('Product'); - - expect(getFederationMetadata(product).externals).toMatchInlineSnapshot(` - Object { - "serviceB--MISSING": Array [ - Object { - "field": sku: String! @external, - "parentTypeName": "Product", - "serviceName": "serviceB--MISSING", - }, - ], - "serviceC--found": Array [ - Object { - "field": sku: String! @external, - "parentTypeName": "Product", - "serviceName": "serviceC--found", - }, - Object { - "field": upc: String! @external, - "parentTypeName": "Product", - "serviceName": "serviceC--found", - }, - ], - } - `); - }); - it('does not redefine fields with @external when composing', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(schema).toBeDefined(); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - - expect(product).toMatchInlineSnapshot(` - type Product { - sku: String! - name: String! - price: Int! - } - `); - expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( - 'serviceB', - ); - expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); - }); - }); - - describe('@requires directive', () => { - it('adds @requires information to fields using a simple field set', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect( - getFederationMetadata(product.getFields()['price']).requires, - ).toMatchInlineSnapshot(`sku`); - }); - - it('adds @requires information to fields using a nested field set', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku { id }") { - sku: Sku! - } - - type Sku { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: Sku! @external - price: Float! @requires(fields: "sku { id }") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(getFederationMetadata(product.getFields()['price']).requires) - .toMatchInlineSnapshot(` - sku { - id - } - `); - }); - }); - - // TODO: provides can happen on an extended type as well, add a test case for this - describe('@provides directive', () => { - it('adds @provides information to fields using a simple field set', () => { - const serviceA = { - typeDefs: gql` - type Review { - product: Product @provides(fields: "sku") - } - - extend type Product { - sku: String @external - color: String - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const review = schema.getType('Review') as GraphQLObjectType; - expect(getFederationMetadata(review.getFields()['product'])).toMatchInlineSnapshot(` - Object { - "belongsToValueType": false, - "provides": sku, - "serviceName": "serviceA", - } - `); - }); - - it('adds @provides information to fields using a nested field set', () => { - const serviceA = { - typeDefs: gql` - type Review { - product: Product @provides(fields: "sku { id }") - } - - extend type Product { - sku: Sku @external - color: String - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product @key(fields: "sku { id }") { - sku: Sku! - price: Int! @requires(fields: "sku") - } - - type Sku { - id: ID! - value: String! - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const review = schema.getType('Review') as GraphQLObjectType; - expect(getFederationMetadata(review.getFields()['product']).provides) - .toMatchInlineSnapshot(` - sku { - id - } - `); - }); - - it('adds @provides information for object types within list types', () => { - const serviceA = { - typeDefs: gql` - type Review { - products: [Product] @provides(fields: "sku") - } - - extend type Product { - sku: String @external - color: String - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const review = schema.getType('Review') as GraphQLObjectType; - expect(getFederationMetadata(review.getFields()['products'])) - .toMatchInlineSnapshot(` - Object { - "belongsToValueType": false, - "provides": sku, - "serviceName": "serviceA", - } - `); - }); - - it('adds correct @provides information to fields on value types', () => { - const serviceA = { - typeDefs: gql` - extend type Query { - valueType: ValueType - } - - type ValueType { - id: ID! - user: User! @provides(fields: "id name") - } - - type User @key(fields: "id") { - id: ID! - name: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type ValueType { - id: ID! - user: User! @provides(fields: "id name") - } - - extend type User @key(fields: "id") { - id: ID! @external - name: String! @external - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const valueType = schema.getType('ValueType') as GraphQLObjectType; - const userFieldFederationMetadata = getFederationMetadata(valueType.getFields()['user']); - expect(userFieldFederationMetadata.belongsToValueType).toBe(true); - expect(userFieldFederationMetadata.serviceName).toBe(null); - }); - }); - - describe('@key directive', () => { - it('adds @key information to types using basic string notation', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") @key(fields: "upc") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(` - Object { - "serviceA": Array [ - sku, - upc, - ], - } - `); - }); - - it('adds @key information to types using selection set notation', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(` - Object { - "serviceA": Array [ - color { - id - value - }, - ], - } - `); - }); - - it('preserves @key information with respect to types across different services', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(` - Object { - "serviceA": Array [ - color { - id - value - }, - ], - "serviceB": Array [ - sku, - ], - } - `); - }); - }); - - describe('@extends directive', () => { - it('treats types with @extends as type extensions', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product @extends @key(fields: "sku") { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const normalizedServices = [serviceA, serviceB].map( - ({ name, typeDefs }) => ({ - name, - typeDefs: normalizeTypeDefs(typeDefs), - }), - ); - const { schema, errors } = composeServices(normalizedServices); - - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(product).toMatchInlineSnapshot(` - type Product { - sku: String! - upc: String! - price: Int! - } - `); - }); - - it('treats interfaces with @extends as interface extensions', () => { - const serviceA = { - typeDefs: gql` - interface Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - interface Product @extends @key(fields: "sku") { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const normalizedServices = [serviceA, serviceB].map( - ({ name, typeDefs }) => ({ - name, - typeDefs: normalizeTypeDefs(typeDefs), - }), - ); - const { schema, errors } = composeServices(normalizedServices); - - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(product).toMatchInlineSnapshot(` - interface Product { - sku: String! - upc: String! - price: Int! - } - `); - }); - }); - }); - describe('executable directives', () => { - it('keeps executable directives in the schema', () => { - const serviceA = { - typeDefs: gql` - directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - `, - name: 'serviceA', - }; - - const { schema, errors } = composeServices([serviceA]); - - expect(errors).toHaveLength(0); - - const defer = schema.getDirective('defer') as GraphQLDirective; - expect(defer).toMatchInlineSnapshot(`"@defer"`); - }); - it('keeps executable directives in the schema', () => { - const serviceA = { - typeDefs: gql` - directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - `, - name: 'serviceA', - }; - const serviceB = { - typeDefs: gql` - directive @stream on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - `, - name: 'serviceB', - }; - - const { schema, errors } = composeServices([serviceA, serviceB]); - - expect(errors).toHaveLength(0); - - const defer = schema.getDirective('defer') as GraphQLDirective; - expect(defer).toMatchInlineSnapshot(`"@defer"`); - - const stream = schema.getDirective('stream') as GraphQLDirective; - expect(stream).toMatchInlineSnapshot(`"@stream"`); - }); - }); -}); - -// XXX Ignored/unimplemented spec tests -// it("allows extension of custom scalars", () => {}); diff --git a/packages/apollo-federation/src/composition/__tests__/composeAndValidate.test.ts b/packages/apollo-federation/src/composition/__tests__/composeAndValidate.test.ts deleted file mode 100644 index a585a56c6c4..00000000000 --- a/packages/apollo-federation/src/composition/__tests__/composeAndValidate.test.ts +++ /dev/null @@ -1,878 +0,0 @@ -import { composeAndValidate } from '../composeAndValidate'; -import gql from 'graphql-tag'; -import { - GraphQLObjectType, - DocumentNode, - GraphQLScalarType, - specifiedDirectives, - printSchema, -} from 'graphql'; -import { - astSerializer, - typeSerializer, - graphqlErrorSerializer, -} from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(typeSerializer); -expect.addSnapshotSerializer(graphqlErrorSerializer); - -const productsService = { - name: 'Products', - typeDefs: gql` - extend type Query { - topProducts(first: Int): [Product] - } - type Product @key(fields: "upc") { - upc: String! - sku: String! - name: String - price: String - } - `, -}; - -const reviewsService = { - name: 'Reviews', - typeDefs: gql` - type Review @key(fields: "id") { - id: ID! - body: String - author: User - product: Product - } - - extend type User @key(fields: "id") { - id: ID! @external - reviews: [Review] - } - extend type Product @key(fields: "upc") { - upc: String! @external - reviews: [Review] - } - `, -}; - -const accountsService = { - name: 'Accounts', - typeDefs: gql` - extend type Query { - me: User - } - type User @key(fields: "id") { - id: ID! - name: String - username: String - birthDate: String - } - `, -}; - -const inventoryService = { - name: 'Inventory', - typeDefs: gql` - extend type Product @key(fields: "upc") { - upc: String! @external - inStock: Boolean - # quantity: Int - } - `, -}; - -function permutateList(inputArr: T[]) { - let result: T[][] = []; - - function permute(arr: T[], m: T[] = []) { - if (arr.length === 0) { - result.push(m); - } else { - for (let i = 0; i < arr.length; i++) { - let curr = arr.slice(); - let next = curr.splice(i, 1); - permute(curr.slice(), m.concat(next)); - } - } - } - - permute(inputArr); - - return result; -} - -it('composes and validates all (24) permutations without error', () => { - permutateList([ - inventoryService, - reviewsService, - accountsService, - productsService, - ]).map((config) => { - const { errors } = composeAndValidate(config); - - if (errors.length) { - console.error( - `Errors found with composition [${config.map((item) => item.name)}]`, - ); - } - - expect(errors).toHaveLength(0); - }); -}); - -it('errors when a type extension has no base', () => { - const serviceA = { - typeDefs: gql` - schema { - query: MyRoot - } - - type MyRoot { - products: [Product]! - } - - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Location { - id: ID - } - `, - name: 'serviceB', - }; - - const { errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(1); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTENSION_WITH_NO_BASE", - "message": "[serviceB] Location -> \`Location\` is an extension type, but \`Location\` is not defined in any service", - }, - ] - `); -}); - -it('treats types with @extends as type extensions', () => { - const serviceA = { - typeDefs: gql` - type Query { - products: [Product]! - } - - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product @extends @key(fields: "sku") { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(product).toMatchInlineSnapshot(` - type Product { - sku: String! - upc: String! - price: Int! - } - `); -}); - -it('treats interfaces with @extends as interface extensions', () => { - const serviceA = { - typeDefs: gql` - type Query { - products: [Product]! - } - - interface Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - interface Product @extends @key(fields: "sku") { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const product = schema.getType('Product') as GraphQLObjectType; - expect(product).toMatchInlineSnapshot(` - interface Product { - sku: String! - upc: String! - price: Int! - } - `); -}); - -it('errors on invalid usages of default operation names', () => { - const serviceA = { - typeDefs: gql` - schema { - query: RootQuery - } - - type RootQuery { - product: Product - } - - type Product @key(fields: "id") { - id: ID! - query: Query - } - - type Query { - invalidUseOfQuery: Boolean - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Query { - validUseOfQuery: Boolean - } - - extend type Product @key(fields: "id") { - id: ID! @external - sku: String - } - `, - name: 'serviceB', - }; - - const { errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "ROOT_QUERY_USED", - "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", - }, - ] - `); -}); - -describe('composition of value types', () => { - function getSchemaWithValueType(valueType: DocumentNode) { - const serviceA = { - typeDefs: gql` - ${valueType} - - type Query { - filler: String - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: valueType, - name: 'serviceB', - }; - - return composeAndValidate([serviceA, serviceB]); - } - - describe('success', () => { - it('scalars', () => { - const { errors, schema } = getSchemaWithValueType( - gql` - scalar Date - `, - ); - expect(errors).toHaveLength(0); - expect(schema.getType('Date')).toMatchInlineSnapshot(`scalar Date`); - }); - - it('unions and object types', () => { - const { errors, schema } = getSchemaWithValueType( - gql` - union CatalogItem = Couch | Mattress - - type Couch { - sku: ID! - material: String! - } - - type Mattress { - sku: ID! - size: String! - } - `, - ); - expect(errors).toHaveLength(0); - expect(schema.getType('CatalogItem')).toMatchInlineSnapshot( - `union CatalogItem = Couch | Mattress`, - ); - expect(schema.getType('Couch')).toMatchInlineSnapshot(` - type Couch { - sku: ID! - material: String! - } - `); - }); - - it('input types', () => { - const { errors, schema } = getSchemaWithValueType(gql` - input NewProductInput { - sku: ID! - type: String - } - `); - expect(errors).toHaveLength(0); - expect(schema.getType('NewProductInput')).toMatchInlineSnapshot(` - input NewProductInput { - sku: ID! - type: String - } - `); - }); - - it('interfaces', () => { - const { errors, schema } = getSchemaWithValueType(gql` - interface Product { - sku: ID! - } - `); - expect(errors).toHaveLength(0); - expect(schema.getType('Product')).toMatchInlineSnapshot(` - interface Product { - sku: ID! - } - `); - }); - - it('enums', () => { - const { errors, schema } = getSchemaWithValueType(gql` - enum CatalogItemEnum { - COUCH - MATTRESS - } - `); - expect(errors).toHaveLength(0); - expect(schema.getType('CatalogItemEnum')).toMatchInlineSnapshot(` - enum CatalogItemEnum { - COUCH - MATTRESS - } - `); - }); - }); - - describe('errors', () => { - it('when used as an entity', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - } - - type Product { - sku: ID! - color: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Query { - topProducts: [Product] - } - - type Product @key(fields: "sku") { - sku: ID! - color: String! - } - `, - name: 'serviceB', - }; - - const { errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_NO_ENTITY", - "message": "[serviceB] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", - } - `); - }); - - it('on field type mismatch', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - } - - type Product { - sku: ID! - color: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Query { - topProducts: [Product] - } - - type Product { - sku: ID! - color: String - } - `, - name: 'serviceB', - }; - - const { errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", - "message": "[serviceA] Product.color -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.color\` as a String! and String respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", - } - `); - }); - - it('on kind mismatch', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - } - - interface Product { - sku: ID! - color: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Query { - topProducts: [Product] - } - - type Product { - sku: ID! - color: String! - } - `, - name: 'serviceB', - }; - - const { errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_KIND_MISMATCH", - "message": "[serviceA] Product -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`Product\` is defined as both a \`ObjectTypeDefinition\` and a \`InterfaceTypeDefinition\`. In order to define \`Product\` in multiple places, the kinds must be identical.", - } - `); - }); - - it('on union types mismatch', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - } - - type Couch { - sku: ID! - } - - type Mattress { - sku: ID! - } - - union Product = Couch | Mattress - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Query { - topProducts: [Product] - } - - type Couch { - sku: ID! - } - - type Cabinet { - sku: ID! - } - - union Product = Couch | Cabinet - `, - name: 'serviceB', - }; - - const { errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", - "message": "[serviceA] Product -> The union \`Product\` is defined in services \`serviceA\` and \`serviceB\`, however their types do not match. Union types with the same name must also consist of identical types. The types Cabinet, Mattress are mismatched.", - } - `); - }); - }); - - it('composed type implements ALL interfaces that value types implement', () => { - const serviceA = { - typeDefs: gql` - interface Node { - id: ID! - } - - interface Named { - name: String - } - - type Product implements Named & Node { - id: ID! - name: String - } - - type Query { - node(id: ID!): Node - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - interface Node { - id: ID! - } - - type Product implements Node { - id: ID! - name: String - } - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - interface Named { - name: String - } - - type Product implements Named { - id: ID! - name: String - } - `, - name: 'serviceC', - }; - - const serviceD = { - typeDefs: gql` - type Product { - id: ID! - name: String - } - `, - name: 'serviceD', - }; - - const { schema, errors, composedSdl } = composeAndValidate([ - serviceA, - serviceB, - serviceC, - serviceD, - ]); - - expect(errors).toHaveLength(0); - expect((schema.getType('Product') as GraphQLObjectType).getInterfaces()) - .toHaveLength(2); - - expect(printSchema(schema)).toContain('type Product implements Named & Node'); - expect(composedSdl).toContain('type Product implements Named & Node'); - - }); -}); - -describe('composition of schemas with directives', () => { - /** - * To see which usage sites indicate whether a directive is "executable" or - * merely for use by the type-system ("type-system"), see the GraphQL spec: - * https://graphql.github.io/graphql-spec/June2018/#sec-Type-System.Directives - */ - it('preserves executable and purges type-system directives', () => { - const serviceA = { - typeDefs: gql` - "directives at FIELDs are executable" - directive @audit(risk: Int!) on FIELD - - "directives at FIELD_DEFINITIONs are for the type-system" - directive @transparency(concealment: Int!) on FIELD_DEFINITION - - type EarthConcern { - environmental: String! @transparency(concealment: 5) - } - - extend type Query { - importantDirectives: [EarthConcern!]! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - "directives at FIELDs are executable" - directive @audit(risk: Int!) on FIELD - - "directives at FIELD_DEFINITIONs are for the type-system" - directive @transparency(concealment: Int!) on FIELD_DEFINITION - - "directives at OBJECTs are for the type-system" - directive @experimental on OBJECT - - extend type EarthConcern @experimental { - societal: String! @transparency(concealment: 6) - } - `, - name: 'serviceB', - }; - - const { schema, errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(0); - - const audit = schema.getDirective('audit'); - expect(audit).toMatchInlineSnapshot(`"@audit"`); - - const transparency = schema.getDirective('transparency'); - expect(transparency).toBeUndefined(); - - const type = schema.getType('EarthConcern') as GraphQLObjectType; - - expect(type.astNode).toMatchInlineSnapshot(` - type EarthConcern { - environmental: String! - } - `); - - const fields = type.getFields(); - - expect(fields['environmental'].astNode).toMatchInlineSnapshot( - `environmental: String!`, - ); - - expect(fields['societal'].astNode).toMatchInlineSnapshot( - `societal: String!`, - ); - }); - - it(`doesn't strip the special case @deprecated and @specifiedBy type-system directives`, () => { - const specUrl = 'http://my-spec-url.com'; - const deprecationReason = "Don't remove me please"; - - // Detecting >15.1.0 by the new addition of the `specifiedBy` directive - const isAtLeastGraphqlVersionFifteenPointOne = - specifiedDirectives.length >= 4; - - const serviceA = { - typeDefs: gql` - # This directive needs to be conditionally added depending on the testing - # environment's version of graphql (>= 15.1.0 includes this new directive) - ${ - isAtLeastGraphqlVersionFifteenPointOne - ? `scalar MyScalar @specifiedBy(url: "${specUrl}")` - : '' - } - - type EarthConcern { - environmental: String! - } - - extend type Query { - importantDirectives: [EarthConcern!]! - @deprecated(reason: "${deprecationReason}") - } - `, - name: 'serviceA', - }; - - const { schema, errors } = composeAndValidate([serviceA]); - expect(errors).toHaveLength(0); - - const deprecated = schema.getDirective('deprecated'); - expect(deprecated).toMatchInlineSnapshot(`"@deprecated"`); - - const queryType = schema.getType('Query') as GraphQLObjectType; - const field = queryType.getFields()['importantDirectives']; - - expect(field.isDeprecated).toBe(true); - expect(field.deprecationReason).toEqual(deprecationReason); - - if (isAtLeastGraphqlVersionFifteenPointOne) { - const specifiedBy = schema.getDirective('specifiedBy'); - expect(specifiedBy).toMatchInlineSnapshot(`"@specifiedBy"`); - const customScalar = schema.getType('MyScalar'); - expect((customScalar as GraphQLScalarType).specifiedByUrl).toEqual( - specUrl, - ); - } - }); -}); - -it('composition of full-SDL schemas without any errors', () => { - const serviceA = { - typeDefs: gql` - # Default directives - directive @deprecated( - reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE - directive @specifiedBy(url: String!) on SCALAR - directive @include( - if: String = "Included when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @skip( - if: String = "Skipped when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - - # Federation directives - directive @key(fields: _FieldSet!) on OBJECT | INTERFACE - directive @external on FIELD_DEFINITION - directive @requires(fields: _FieldSet!) on FIELD_DEFINITION - directive @provides(fields: _FieldSet!) on FIELD_DEFINITION - directive @extends on OBJECT | INTERFACE - - # Custom type system directive (disregarded by gateway, unconcerned with serviceB's implementation) - directive @myTypeSystemDirective on FIELD_DEFINITION - # Custom executable directive (must be implemented in all services, definition must be identical) - directive @myExecutableDirective on FIELD - - scalar _Any - scalar _FieldSet - - union _Entity - - type _Service { - sdl: String - } - - schema { - query: RootQuery - mutation: RootMutation - } - - type RootQuery { - _service: _Service! - _entities(representations: [_Any!]!): [_Entity]! - product: Product - } - - type Product @key(fields: "sku") { - sku: String! - price: Float - } - - type RootMutation { - updateProduct: Product - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - # Default directives - directive @deprecated( - reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE - directive @specifiedBy(url: String!) on SCALAR - directive @include( - if: String = "Included when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @skip( - if: String = "Skipped when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - - # Federation directives - directive @key(fields: _FieldSet!) on OBJECT | INTERFACE - directive @external on FIELD_DEFINITION - directive @requires(fields: _FieldSet!) on FIELD_DEFINITION - directive @provides(fields: _FieldSet!) on FIELD_DEFINITION - directive @extends on OBJECT | INTERFACE - - # Custom type system directive (disregarded by gateway, unconcerned with serviceA's implementation) - directive @myDirective on FIELD_DEFINITION - - # Custom executable directive (must be implemented in all services, definition must be identical) - directive @myExecutableDirective on FIELD - - scalar _Any - scalar _FieldSet - - union _Entity - - type _Service { - sdl: String - } - - type Query { - _service: _Service! - _entities(representations: [_Any!]!): [_Entity]! - review: Review - } - - type Review @key(fields: "id") { - id: String! - content: String - } - - type Mutation { - createReview: Review - } - `, - name: 'serviceB', - }; - - const { errors } = composeAndValidate([serviceA, serviceB]); - expect(errors).toHaveLength(0); -}); diff --git a/packages/apollo-federation/src/composition/__tests__/normalize.test.ts b/packages/apollo-federation/src/composition/__tests__/normalize.test.ts deleted file mode 100644 index 6ce8b5367d7..00000000000 --- a/packages/apollo-federation/src/composition/__tests__/normalize.test.ts +++ /dev/null @@ -1,410 +0,0 @@ -import gql from 'graphql-tag'; -import { - defaultRootOperationTypes, - replaceExtendedDefinitionsWithExtensions, - normalizeTypeDefs, - stripCommonPrimitives, -} from '../normalize'; -import { astSerializer } from '../../snapshotSerializers'; -import { specifiedDirectives } from 'graphql'; - -expect.addSnapshotSerializer(astSerializer); - -describe('SDL normalization and its respective parts', () => { - describe('defaultRootOperationTypes', () => { - it('transforms defined root operation types to respective extended default root operation types', () => { - const typeDefs = gql` - schema { - query: RootQuery - mutation: RootMutation - } - - type RootQuery { - product: Product - } - - type Product { - sku: String - } - - type RootMutation { - updateProduct: Product - } - `; - - const schemaWithDefaultedRootOperationTypes = defaultRootOperationTypes( - typeDefs, - ); - expect(schemaWithDefaultedRootOperationTypes).toMatchInlineSnapshot(` - extend type Query { - product: Product - } - - type Product { - sku: String - } - - extend type Mutation { - updateProduct: Product - } - `); - }); - - it('removes all types using a default root operation type name when a schema definition is provided (root types are defined by the user)', () => { - const typeDefs = gql` - schema { - query: RootQuery - } - - type RootQuery { - product: Product - } - - type Product { - sku: String - } - - type Query { - removeThisEntireType: String - } - - type Mutation { - removeThisEntireType: String - } - - type Subscription { - removeThisEntireType: String - } - `; - - const schemaWithDefaultedRootOperationTypes = defaultRootOperationTypes( - typeDefs, - ); - expect(schemaWithDefaultedRootOperationTypes).toMatchInlineSnapshot(` - extend type Query { - product: Product - } - - type Product { - sku: String - } - `); - }); - - it('drops fields that reference an invalid default root operation type name', () => { - const typeDefs = gql` - schema { - query: RootQuery - mutation: RootMutation - } - - type RootQuery { - product: Product - } - - type Query { - removeThisEntireType: String - } - - type RootMutation { - keepThisField: String - removeThisField: Query - } - `; - - const schemaWithDefaultedRootOperationTypes = defaultRootOperationTypes( - typeDefs, - ); - expect(schemaWithDefaultedRootOperationTypes).toMatchInlineSnapshot(` - extend type Query { - product: Product - } - - extend type Mutation { - keepThisField: String - } - `); - }); - }); - - describe('replaceExtendedDefinitionsWithExtensions', () => { - it('transforms the @extends directive into type extensions', () => { - const typeDefs = gql` - type Product @extends @key(fields: "sku") { - sku: String @external - } - `; - - expect(replaceExtendedDefinitionsWithExtensions(typeDefs)) - .toMatchInlineSnapshot(` - extend type Product @key(fields: "sku") { - sku: String @external - } - `); - }); - }); - - describe('stripCommonPrimitives', () => { - it(`removes all common directive definitions`, () => { - // Detecting >15.1.0 by the new addition of the `specifiedBy` directive - const isAtLeastGraphqlVersionFifteenPointOne = - specifiedDirectives.length >= 4; - - const typeDefs = gql` - # Default directives - - # This directive needs to be conditionally added depending on the testing - # environment's version of graphql (>= 15.1.0 includes this new directive) - ${isAtLeastGraphqlVersionFifteenPointOne - ? 'directive @specifiedBy(url: String!) on SCALAR' - : ''} - directive @deprecated( - reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE - directive @include( - if: String = "Included when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @skip( - if: String = "Skipped when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - - # Federation directives - directive @key(fields: _FieldSet!) on OBJECT | INTERFACE - directive @external on FIELD_DEFINITION - directive @requires(fields: _FieldSet!) on FIELD_DEFINITION - directive @provides(fields: _FieldSet!) on FIELD_DEFINITION - directive @extends on OBJECT | INTERFACE - - type Query { - thing: String - } - `; - - expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` - type Query { - thing: String - } - `); - }); - - it(`doesn't remove custom directive definitions`, () => { - const typeDefs = gql` - directive @custom on OBJECT - - type Query { - thing: String - } - `; - - expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` - directive @custom on OBJECT - - type Query { - thing: String - } - `); - }); - - it(`removes all federation type definitions (scalars, unions, object types)`, () => { - const typeDefs = gql` - scalar _Any - scalar _FieldSet - - union _Entity - - type _Service { - sdl: String - } - - type Query { - thing: String - } - `; - - expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` - type Query { - thing: String - } - `); - }); - - it(`doesn't remove custom scalar, union, or object type definitions`, () => { - const typeDefs = gql` - scalar CustomScalar - - type CustomType { - field: String! - } - - union CustomUnion - - type Query { - thing: String - } - `; - - expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` - scalar CustomScalar - - type CustomType { - field: String! - } - - union CustomUnion - - type Query { - thing: String - } - `); - }); - - it(`removes all federation field definitions (_service, _entities)`, () => { - const typeDefs = gql` - type Query { - _service: _Service! - _entities(representations: [_Any!]!): [_Entity]! - thing: String - } - `; - - expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` - type Query { - thing: String - } - `); - }); - - it(`removes the Query type altogether if it has no fields left after normalization`, () => { - const typeDefs = gql` - type Query { - _service: _Service! - _entities(representations: [_Any!]!): [_Entity]! - } - - type Custom { - field: String - } - `; - - expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` - type Custom { - field: String - } - `); - }); - }); - - describe('normalizeTypeDefs', () => { - it('integration', () => { - // Detecting >15.1.0 by the new addition of the `specifiedBy` directive - const isAtLeastGraphqlVersionFifteenPointOne = - specifiedDirectives.length >= 4; - - const typeDefsToNormalize = gql` - # Default directives - - # This directive needs to be conditionally added depending on the testing - # environment's version of graphql (>= 15.1.0 includes this new directive) - ${isAtLeastGraphqlVersionFifteenPointOne - ? 'directive @specifiedBy(url: String!) on SCALAR' - : ''} - directive @deprecated( - reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE - directive @include( - if: String = "Included when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @skip( - if: String = "Skipped when true." - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - - directive @key(fields: _FieldSet!) on OBJECT | INTERFACE - directive @external on FIELD_DEFINITION - directive @requires(fields: _FieldSet!) on FIELD_DEFINITION - directive @provides(fields: _FieldSet!) on FIELD_DEFINITION - directive @extends on OBJECT | INTERFACE - - scalar _Any - scalar _FieldSet - - union _Entity - - type _Service { - sdl: String - } - - schema { - query: RootQuery - mutation: RootMutation - } - - type RootQuery { - _service: _Service! - _entities(representations: [_Any!]!): [_Entity]! - product: Product - } - - type Product @extends @key(fields: "sku") { - sku: String @external - } - - type RootMutation { - updateProduct: Product - } - `; - - const normalized = normalizeTypeDefs(typeDefsToNormalize); - - expect(normalized).toMatchInlineSnapshot(` - extend type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: String @external - } - - extend type Mutation { - updateProduct: Product - } - `); - }); - - it('should allow schema describing default types', () => { - const typeDefsToNormalize = gql` - schema { - query: Query - mutation: Mutation - } - - type Query { - product: Product - } - - type Product @extends @key(fields: "sku") { - sku: String @external - } - - type Mutation { - updateProduct: Product - } - `; - - const normalized = normalizeTypeDefs(typeDefsToNormalize); - - expect(normalized).toMatchInlineSnapshot(` - extend type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: String @external - } - - extend type Mutation { - updateProduct: Product - } - `); - }); - }); -}); diff --git a/packages/apollo-federation/src/composition/__tests__/tsconfig.json b/packages/apollo-federation/src/composition/__tests__/tsconfig.json deleted file mode 100644 index 16b741c8c51..00000000000 --- a/packages/apollo-federation/src/composition/__tests__/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../../../tsconfig.test.base", - "compilerOptions": { - "strictNullChecks": false - }, - "include": ["**/*"], - "references": [{ "path": "../../../../../" }] -} diff --git a/packages/apollo-federation/src/composition/__tests__/utils.test.ts b/packages/apollo-federation/src/composition/__tests__/utils.test.ts deleted file mode 100644 index 90164b06a16..00000000000 --- a/packages/apollo-federation/src/composition/__tests__/utils.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import gql from 'graphql-tag'; -import deepFreeze from 'deep-freeze'; -import { stripExternalFieldsFromTypeDefs } from '../utils'; -import { astSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); - -describe('Composition utility functions', () => { - describe('stripExternalFieldsFromTypeDefs', () => { - it('returns a new DocumentNode with @external fields removed as well as information about the removed fields', () => { - const typeDefs = gql` - type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: String @external - } - - type Mutation { - updateProduct: Product - } - - extend interface Account @key(fields: "id") { - id: ID! @external - } - `; - - const { - typeDefsWithoutExternalFields, - strippedFields, - } = stripExternalFieldsFromTypeDefs(typeDefs, 'serviceA'); - - expect(typeDefsWithoutExternalFields).toMatchInlineSnapshot(` - type Query { - product: Product - } - - extend type Product @key(fields: "sku") - - type Mutation { - updateProduct: Product - } - - extend interface Account @key(fields: "id") - `); - - expect(strippedFields).toMatchInlineSnapshot(` - Array [ - Object { - "field": sku: String @external, - "parentTypeName": "Product", - "serviceName": "serviceA", - }, - Object { - "field": id: ID! @external, - "parentTypeName": "Account", - "serviceName": "serviceA", - }, - ] - `); - }); - - it("doesn't mutate the input DocumentNode", () => { - const typeDefs = gql` - type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: String @external - } - - type Mutation { - updateProduct: Product - } - `; - - deepFreeze(typeDefs); - - // Assert that mutation does, in fact, throw - expect(() => (typeDefs.blah = [])).toThrow(); - expect(() => - stripExternalFieldsFromTypeDefs(typeDefs, 'serviceA'), - ).not.toThrow(); - }); - }); -}); diff --git a/packages/apollo-federation/src/composition/compose.ts b/packages/apollo-federation/src/composition/compose.ts deleted file mode 100644 index 1f7461d2877..00000000000 --- a/packages/apollo-federation/src/composition/compose.ts +++ /dev/null @@ -1,652 +0,0 @@ -import 'apollo-server-env'; -import { - GraphQLSchema, - extendSchema, - Kind, - isTypeDefinitionNode, - isTypeExtensionNode, - GraphQLError, - GraphQLNamedType, - isObjectType, - FieldDefinitionNode, - InputValueDefinitionNode, - DocumentNode, - GraphQLObjectType, - specifiedDirectives, - TypeDefinitionNode, - DirectiveDefinitionNode, - TypeExtensionNode, - ObjectTypeDefinitionNode, - NamedTypeNode, -} from 'graphql'; -import { transformSchema } from 'apollo-graphql'; -import federationDirectives from '../directives'; -import { - findDirectivesOnTypeOrField, - isStringValueNode, - parseSelections, - mapFieldNamesToServiceName, - stripExternalFieldsFromTypeDefs, - typeNodesAreEquivalent, - mapValues, - isFederationDirective, - executableDirectiveLocations, - stripTypeSystemDirectivesFromTypeDefs, - defaultRootOperationNameLookup, - getFederationMetadata, -} from './utils'; -import { - ServiceDefinition, - ExternalFieldDefinition, - ServiceNameToKeyDirectivesMap, - FederationType, - FederationField, - FederationDirective, -} from './types'; -import { validateSDL } from 'graphql/validation/validate'; -import { compositionRules } from './rules'; - -const EmptyQueryDefinition = { - kind: Kind.OBJECT_TYPE_DEFINITION, - name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.query }, - fields: [], - serviceName: null, -}; -const EmptyMutationDefinition = { - kind: Kind.OBJECT_TYPE_DEFINITION, - name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.mutation }, - fields: [], - serviceName: null, -}; - -// Map of all type definitions to eventually be passed to extendSchema -interface TypeDefinitionsMap { - [name: string]: TypeDefinitionNode[]; -} -// Map of all type extensions to eventually be passed to extendSchema -interface TypeExtensionsMap { - [name: string]: TypeExtensionNode[]; -} - -// Map of all directive definitions to eventually be passed to extendSchema -interface DirectiveDefinitionsMap { - [name: string]: { [serviceName: string]: DirectiveDefinitionNode }; -} - -/** - * A map of base types to their owning service. Used by query planner to direct traffic. - * This contains the base type's "owner". Any fields that extend this type in another service - * are listed under "extensionFieldsToOwningServiceMap". extensionFieldsToOwningServiceMap are in the format { myField: my-service-name } - * - * Example resulting typeToServiceMap shape: - * - * const typeToServiceMap = { - * Product: { - * serviceName: "ProductService", - * extensionFieldsToOwningServiceMap: { - * reviews: "ReviewService", // Product.reviews comes from the ReviewService - * dimensions: "ShippingService", - * weight: "ShippingService" - * } - * } - * } - */ -interface TypeToServiceMap { - [typeName: string]: { - owningService?: string; - extensionFieldsToOwningServiceMap: { [fieldName: string]: string }; - }; -} - -/* - * Map of types to their key directives (maintains association to their services) - * - * Example resulting KeyDirectivesMap shape: - * - * const keyDirectives = { - * Product: { - * serviceA: ["sku", "upc"] - * serviceB: ["color {id value}"] // Selection node simplified for readability - * } - * } - */ -export interface KeyDirectivesMap { - [typeName: string]: ServiceNameToKeyDirectivesMap; -} - -/** - * A set of type names that have been determined to be a value type, a type - * shared across at least 2 services. - */ -type ValueTypes = Set; -/** - * Loop over each service and process its typeDefs (`definitions`) - * - build up typeToServiceMap - * - push individual definitions onto either typeDefinitionsMap or typeExtensionsMap - */ -export function buildMapsFromServiceList(serviceList: ServiceDefinition[]) { - const typeDefinitionsMap: TypeDefinitionsMap = Object.create(null); - const typeExtensionsMap: TypeExtensionsMap = Object.create(null); - const directiveDefinitionsMap: DirectiveDefinitionsMap = Object.create(null); - const typeToServiceMap: TypeToServiceMap = Object.create(null); - const externalFields: ExternalFieldDefinition[] = []; - const keyDirectivesMap: KeyDirectivesMap = Object.create(null); - const valueTypes: ValueTypes = new Set(); - - for (const { typeDefs, name: serviceName } of serviceList) { - // Build a new SDL with @external fields removed, as well as information about - // the fields that were removed. - const { - typeDefsWithoutExternalFields, - strippedFields, - } = stripExternalFieldsFromTypeDefs(typeDefs, serviceName); - - externalFields.push(...strippedFields); - - // Type system directives from downstream services are not a concern of the - // gateway, but rather the services on which the fields live which serve - // those types. In other words, its up to an implementing service to - // act on such directives, not the gateway. - const typeDefsWithoutTypeSystemDirectives = - stripTypeSystemDirectivesFromTypeDefs(typeDefsWithoutExternalFields); - - for (const definition of typeDefsWithoutTypeSystemDirectives.definitions) { - if ( - definition.kind === Kind.OBJECT_TYPE_DEFINITION || - definition.kind === Kind.OBJECT_TYPE_EXTENSION - ) { - const typeName = definition.name.value; - - for (const keyDirective of findDirectivesOnTypeOrField( - definition, - 'key', - )) { - if ( - keyDirective.arguments && - isStringValueNode(keyDirective.arguments[0].value) - ) { - // Initialize the entry for this type if necessary - keyDirectivesMap[typeName] = keyDirectivesMap[typeName] || {}; - // Initialize the entry for this service if necessary - keyDirectivesMap[typeName][serviceName] = - keyDirectivesMap[typeName][serviceName] || []; - // Add @key metadata to the array - keyDirectivesMap[typeName][serviceName].push( - parseSelections(keyDirective.arguments[0].value.value), - ); - } - } - } - - if (isTypeDefinitionNode(definition)) { - const typeName = definition.name.value; - /** - * This type is a base definition (not an extension). If this type is already in the typeToServiceMap, then - * 1. It was declared by a previous service, but this newer one takes precedence, or... - * 2. It was extended by a service before declared - */ - if (!typeToServiceMap[typeName]) { - typeToServiceMap[typeName] = { - extensionFieldsToOwningServiceMap: Object.create(null), - }; - } - - typeToServiceMap[typeName].owningService = serviceName; - - /** - * If this type already exists in the definitions map, push this definition to the array (newer defs - * take precedence). If the types are determined to be identical, add the type name - * to the valueTypes Set. - * - * If not, create the definitions array and add it to the typeDefinitionsMap. - */ - if (typeDefinitionsMap[typeName]) { - const isValueType = typeNodesAreEquivalent( - typeDefinitionsMap[typeName][ - typeDefinitionsMap[typeName].length - 1 - ], - definition, - ); - - if (isValueType) { - valueTypes.add(typeName); - } - - typeDefinitionsMap[typeName].push({ ...definition, serviceName }); - } else { - typeDefinitionsMap[typeName] = [{ ...definition, serviceName }]; - } - } else if (isTypeExtensionNode(definition)) { - const typeName = definition.name.value; - - /** - * This definition is an extension of an OBJECT type defined in another service. - * TODO: handle extensions of non-object types? - */ - if ( - definition.kind === Kind.OBJECT_TYPE_EXTENSION || - definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION - ) { - if (!definition.fields) break; - const fields = mapFieldNamesToServiceName< - FieldDefinitionNode | InputValueDefinitionNode - >(definition.fields, serviceName); - - /** - * If the type already exists in the typeToServiceMap, add the extended fields. If not, create the object - * and add the extensionFieldsToOwningServiceMap, but don't add a serviceName. That will be added once that service - * definition is processed. - */ - if (typeToServiceMap[typeName]) { - typeToServiceMap[typeName].extensionFieldsToOwningServiceMap = { - ...typeToServiceMap[typeName].extensionFieldsToOwningServiceMap, - ...fields, - }; - } else { - typeToServiceMap[typeName] = { - extensionFieldsToOwningServiceMap: fields, - }; - } - } - - if (definition.kind === Kind.ENUM_TYPE_EXTENSION) { - if (!definition.values) break; - - const values = mapFieldNamesToServiceName( - definition.values, - serviceName, - ); - - if (typeToServiceMap[typeName]) { - typeToServiceMap[typeName].extensionFieldsToOwningServiceMap = { - ...typeToServiceMap[typeName].extensionFieldsToOwningServiceMap, - ...values, - }; - } else { - typeToServiceMap[typeName] = { - extensionFieldsToOwningServiceMap: values, - }; - } - } - - /** - * If an extension for this type already exists in the extensions map, push this extension to the - * array (since a type can be extended by multiple services). If not, create the extensions array - * and add it to the typeExtensionsMap. - */ - if (typeExtensionsMap[typeName]) { - typeExtensionsMap[typeName].push({ ...definition, serviceName }); - } else { - typeExtensionsMap[typeName] = [{ ...definition, serviceName }]; - } - } else if (definition.kind === Kind.DIRECTIVE_DEFINITION) { - const directiveName = definition.name.value; - - // The composed schema should only contain directives and their - // ExecutableDirectiveLocations. This filters out any TypeSystemDirectiveLocations. - // A new DirectiveDefinitionNode with this filtered list will be what is - // added to the schema. - const executableLocations = definition.locations.filter(location => - executableDirectiveLocations.includes(location.value), - ); - - // If none of the directive's locations are executable, we don't need to - // include it in the composed schema at all. - if (executableLocations.length === 0) continue; - - const definitionWithExecutableLocations: DirectiveDefinitionNode = { - ...definition, - locations: executableLocations, - }; - - if (directiveDefinitionsMap[directiveName]) { - directiveDefinitionsMap[directiveName][ - serviceName - ] = definitionWithExecutableLocations; - } else { - directiveDefinitionsMap[directiveName] = { - [serviceName]: definitionWithExecutableLocations, - }; - } - } - } - } - - // Since all Query/Mutation definitions in service schemas are treated as - // extensions, we don't have a Query or Mutation DEFINITION in the definitions - // list. Without a Query/Mutation definition, we can't _extend_ the type. - // extendSchema will complain about this. We can't add an empty - // GraphQLObjectType to the schema constructor, so we add an empty definition - // here. We only add mutation if there is a mutation extension though. - if (!typeDefinitionsMap.Query) - typeDefinitionsMap.Query = [EmptyQueryDefinition]; - if (typeExtensionsMap.Mutation && !typeDefinitionsMap.Mutation) - typeDefinitionsMap.Mutation = [EmptyMutationDefinition]; - - return { - typeToServiceMap, - typeDefinitionsMap, - typeExtensionsMap, - directiveDefinitionsMap, - externalFields, - keyDirectivesMap, - valueTypes, - }; -} - -export function buildSchemaFromDefinitionsAndExtensions({ - typeDefinitionsMap, - typeExtensionsMap, - directiveDefinitionsMap, -}: { - typeDefinitionsMap: TypeDefinitionsMap; - typeExtensionsMap: TypeExtensionsMap; - directiveDefinitionsMap: DirectiveDefinitionsMap; -}) { - let errors: GraphQLError[] | undefined = undefined; - - let schema = new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }); - - // This interface and predicate is a TS / graphql-js workaround for now while - // we're using a local graphql version < v15. This predicate _could_ be: - // `node is ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode` in the - // future to be more semantic. However this gives us type safety and flexibility - // for now. - interface HasInterfaces { - interfaces?: ObjectTypeDefinitionNode['interfaces']; - } - - function nodeHasInterfaces(node: any): node is HasInterfaces { - return 'interfaces' in node; - } - - // Extend the blank schema with the base type definitions (as an AST node) - const definitionsDocument: DocumentNode = { - kind: Kind.DOCUMENT, - definitions: [ - ...Object.values(typeDefinitionsMap).flatMap(typeDefinitions => { - // See if any of our Objects or Interfaces implement any interfaces at all. - // If not, we can return early. - if (!typeDefinitions.some(nodeHasInterfaces)) return typeDefinitions; - - const uniqueInterfaces: Map< - string, - NamedTypeNode - > = (typeDefinitions as HasInterfaces[]).reduce( - (map, objectTypeDef) => { - objectTypeDef.interfaces?.forEach((iface) => - map.set(iface.name.value, iface), - ); - return map; - }, - new Map(), - ); - - // No interfaces, no aggregation - just return what we got. - if (uniqueInterfaces.size === 0) return typeDefinitions; - - const [first, ...rest] = typeDefinitions; - - return [ - ...rest, - { - ...first, - interfaces: Array.from(uniqueInterfaces.values()), - }, - ]; - - }), - ...Object.values(directiveDefinitionsMap).map( - definitions => Object.values(definitions)[0], - ), - ], - }; - - errors = validateSDL(definitionsDocument, schema, compositionRules); - schema = extendSchema(schema, definitionsDocument, { assumeValidSDL: true }); - - // Extend the schema with the extension definitions (as an AST node) - const extensionsDocument: DocumentNode = { - kind: Kind.DOCUMENT, - definitions: Object.values(typeExtensionsMap).flat(), - }; - - errors.push(...validateSDL(extensionsDocument, schema, compositionRules)); - - schema = extendSchema(schema, extensionsDocument, { assumeValidSDL: true }); - - // Remove federation directives from the final schema - schema = new GraphQLSchema({ - ...schema.toConfig(), - directives: [ - ...schema.getDirectives().filter(x => !isFederationDirective(x)), - ], - }); - - return { schema, errors }; -} - -/** - * Using the various information we've collected about the schema, augment the - * `schema` itself with `federation` metadata to the types and fields - */ -export function addFederationMetadataToSchemaNodes({ - schema, - typeToServiceMap, - externalFields, - keyDirectivesMap, - valueTypes, - directiveDefinitionsMap, -}: { - schema: GraphQLSchema; - typeToServiceMap: TypeToServiceMap; - externalFields: ExternalFieldDefinition[]; - keyDirectivesMap: KeyDirectivesMap; - valueTypes: ValueTypes; - directiveDefinitionsMap: DirectiveDefinitionsMap; -}) { - for (const [ - typeName, - { owningService, extensionFieldsToOwningServiceMap }, - ] of Object.entries(typeToServiceMap)) { - const namedType = schema.getType(typeName) as GraphQLNamedType; - if (!namedType) continue; - - // Extend each type in the GraphQLSchema with the serviceName that owns it - // and the key directives that belong to it - const isValueType = valueTypes.has(typeName); - const serviceName = isValueType ? null : owningService; - - const federationMetadata: FederationType = { - ...getFederationMetadata(namedType), - serviceName, - isValueType, - ...(keyDirectivesMap[typeName] && { - keys: keyDirectivesMap[typeName], - }), - } - - namedType.extensions = { - ...namedType.extensions, - federation: federationMetadata, - }; - - // For object types, add metadata for all the @provides directives from its fields - if (isObjectType(namedType)) { - for (const field of Object.values(namedType.getFields())) { - const [providesDirective] = findDirectivesOnTypeOrField( - field.astNode, - 'provides', - ); - - if ( - providesDirective && - providesDirective.arguments && - isStringValueNode(providesDirective.arguments[0].value) - ) { - const fieldFederationMetadata: FederationField = { - ...getFederationMetadata(field), - serviceName, - provides: parseSelections( - providesDirective.arguments[0].value.value, - ), - belongsToValueType: isValueType, - } - - field.extensions = { - ...field.extensions, - federation: fieldFederationMetadata - }; - } - } - } - - /** - * For extension fields, do 2 things: - * 1. Add serviceName metadata to all fields that belong to a type extension - * 2. add metadata from the @requires directive for each field extension - */ - for (const [fieldName, extendingServiceName] of Object.entries( - extensionFieldsToOwningServiceMap, - )) { - // TODO: Why don't we need to check for non-object types here - if (isObjectType(namedType)) { - const field = namedType.getFields()[fieldName]; - - const fieldFederationMetadata: FederationField = { - ...getFederationMetadata(field), - serviceName: extendingServiceName, - } - - field.extensions = { - ...field.extensions, - federation: fieldFederationMetadata, - }; - - const [requiresDirective] = findDirectivesOnTypeOrField( - field.astNode, - 'requires', - ); - - if ( - requiresDirective && - requiresDirective.arguments && - isStringValueNode(requiresDirective.arguments[0].value) - ) { - const fieldFederationMetadata: FederationField = { - ...getFederationMetadata(field), - requires: parseSelections( - requiresDirective.arguments[0].value.value, - ), - } - - field.extensions = { - ...field.extensions, - federation: fieldFederationMetadata, - }; - } - } - } - } - // add externals metadata - for (const field of externalFields) { - const namedType = schema.getType(field.parentTypeName); - if (!namedType) continue; - - const existingMetadata = getFederationMetadata(namedType); - const typeFederationMetadata: FederationType = { - ...existingMetadata, - externals: { - ...existingMetadata?.externals, - [field.serviceName]: [ - ...(existingMetadata?.externals?.[field.serviceName] || []), - field, - ], - }, - }; - - namedType.extensions = { - ...namedType.extensions, - federation: typeFederationMetadata, - }; - } - - // add all definitions of a specific directive for validation later - for (const directiveName of Object.keys(directiveDefinitionsMap)) { - const directive = schema.getDirective(directiveName); - if (!directive) continue; - - const directiveFederationMetadata: FederationDirective = { - ...getFederationMetadata(directive), - directiveDefinitions: directiveDefinitionsMap[directiveName], - } - - directive.extensions = { - ...directive.extensions, - federation: directiveFederationMetadata, - } - } -} - -export function composeServices(services: ServiceDefinition[]) { - const { - typeToServiceMap, - typeDefinitionsMap, - typeExtensionsMap, - directiveDefinitionsMap, - externalFields, - keyDirectivesMap, - valueTypes, - } = buildMapsFromServiceList(services); - - let { schema, errors } = buildSchemaFromDefinitionsAndExtensions({ - typeDefinitionsMap, - typeExtensionsMap, - directiveDefinitionsMap, - }); - - // TODO: We should fix this to take non-default operation root types in - // implementing services into account. - schema = new GraphQLSchema({ - ...schema.toConfig(), - ...mapValues(defaultRootOperationNameLookup, typeName => - typeName - ? (schema.getType(typeName) as GraphQLObjectType) - : undefined, - ), - }); - - // If multiple type definitions and extensions for the same type implement the - // same interface, it will get added to the constructed object multiple times, - // resulting in a schema validation error. We therefore need to remove - // duplicate interfaces from object types manually. - schema = transformSchema(schema, type => { - if (isObjectType(type)) { - const config = type.toConfig(); - return new GraphQLObjectType({ - ...config, - interfaces: Array.from(new Set(config.interfaces)), - }); - } - return undefined; - }); - - addFederationMetadataToSchemaNodes({ - schema, - typeToServiceMap, - externalFields, - keyDirectivesMap, - valueTypes, - directiveDefinitionsMap, - }); - - /** - * At the end, we're left with a full GraphQLSchema that _also_ has `serviceName` fields for every type, - * and every field that was extended. Fields that were _not_ extended (added on the base type by the owner), - * there is no `serviceName`, and we should refer to the type's `serviceName` - */ - return { schema, errors }; -} diff --git a/packages/apollo-federation/src/composition/composeAndValidate.ts b/packages/apollo-federation/src/composition/composeAndValidate.ts deleted file mode 100644 index d53229bf81b..00000000000 --- a/packages/apollo-federation/src/composition/composeAndValidate.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { composeServices } from './compose'; -import { - validateComposedSchema, - validateServicesBeforeComposition, - validateServicesBeforeNormalization, -} from './validate'; -import { ServiceDefinition } from './types'; -import { normalizeTypeDefs } from './normalize'; -import { printComposedSdl } from '../service/printComposedSdl'; - -export function composeAndValidate(serviceList: ServiceDefinition[]) { - const errors = validateServicesBeforeNormalization(serviceList); - - const normalizedServiceList = serviceList.map(({ name, typeDefs }) => ({ - name, - typeDefs: normalizeTypeDefs(typeDefs), - })); - - // generate errors or warnings of the individual services - errors.push(...validateServicesBeforeComposition(normalizedServiceList)); - - // generate a schema and any errors or warnings - const compositionResult = composeServices(normalizedServiceList); - errors.push(...compositionResult.errors); - - // validate the composed schema based on service information - errors.push( - ...validateComposedSchema({ - schema: compositionResult.schema, - serviceList, - }), - ); - - // We shouldn't try to print the SDL if there were errors during composition - const composedSdl = - errors.length === 0 - ? printComposedSdl(compositionResult.schema, serviceList) - : undefined; - - // TODO remove the warnings array once no longer used by clients - return { - schema: compositionResult.schema, - warnings: [], - errors, - composedSdl, - }; -} diff --git a/packages/apollo-federation/src/composition/index.ts b/packages/apollo-federation/src/composition/index.ts deleted file mode 100644 index 58c910b8a25..00000000000 --- a/packages/apollo-federation/src/composition/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './compose'; -export * from './composeAndValidate'; -export * from './types'; -export { compositionRules } from './rules'; -export { normalizeTypeDefs } from './normalize'; -export { defaultRootOperationNameLookup } from './utils'; diff --git a/packages/apollo-federation/src/composition/normalize.ts b/packages/apollo-federation/src/composition/normalize.ts deleted file mode 100644 index 9f8fcacb243..00000000000 --- a/packages/apollo-federation/src/composition/normalize.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { DefaultRootOperationTypeName } from './types'; -import { - DocumentNode, - visit, - ObjectTypeDefinitionNode, - ObjectTypeExtensionNode, - Kind, - InterfaceTypeDefinitionNode, - VisitFn, - specifiedDirectives, -} from 'graphql'; -import { - findDirectivesOnTypeOrField, - defKindToExtKind, - reservedRootFields, - defaultRootOperationNameLookup -} from './utils'; -import federationDirectives from '../directives'; - -export function normalizeTypeDefs(typeDefs: DocumentNode) { - // The order of this is important - `stripCommonPrimitives` must come after - // `defaultRootOperationTypes` because it depends on the `Query` type being named - // its default: `Query`. - return stripCommonPrimitives( - defaultRootOperationTypes( - replaceExtendedDefinitionsWithExtensions(typeDefs), - ), - ); -} - -export function defaultRootOperationTypes( - typeDefs: DocumentNode, -): DocumentNode { - // Array of default root operation names - const defaultRootOperationNames = Object.values( - defaultRootOperationNameLookup, - ); - - // Map of the given root operation type names to their respective default operation - // type names, i.e. {RootQuery: 'Query'} - let rootOperationTypeMap: { - [key: string]: DefaultRootOperationTypeName; - } = Object.create(null); - - let hasSchemaDefinitionOrExtension = false; - visit(typeDefs, { - OperationTypeDefinition(node) { - // If we find at least one root operation type definition, we know the user has - // specified either a schema definition or extension. - hasSchemaDefinitionOrExtension = true; - // Build the map of root operation type name to its respective default - rootOperationTypeMap[node.type.name.value] = - defaultRootOperationNameLookup[node.operation]; - }, - }); - - // In this case, there's no defined schema or schema extension, so we use defaults - if (!hasSchemaDefinitionOrExtension) { - rootOperationTypeMap = { - Query: 'Query', - Mutation: 'Mutation', - Subscription: 'Subscription', - }; - } - - // A conflicting default definition exists when the user provides a schema - // definition, but also defines types that use the default root operation - // names (Query, Mutation, Subscription). Those types need to be removed. - let schemaWithoutConflictingDefaultDefinitions; - if (!hasSchemaDefinitionOrExtension) { - // If no schema definition or extension exists, then there aren't any - // conflicting defaults to worry about. - schemaWithoutConflictingDefaultDefinitions = typeDefs; - } else { - // If the user provides a schema definition or extension, then using default - // root operation names is considered an error for composition. This visit - // drops the invalid type definitions/extensions altogether, as well as - // fields that reference them. - // - // Example: - // - // schema { - // query: RootQuery - // } - // - // type Query { <--- this type definition is invalid (as well as Mutation or Subscription) - // ... - // } - schemaWithoutConflictingDefaultDefinitions = visit(typeDefs, { - ObjectTypeDefinition(node) { - if ( - (defaultRootOperationNames as string[]).includes(node.name.value) && - !rootOperationTypeMap[node.name.value] - ) { - return null; - } - return; - }, - ObjectTypeExtension(node) { - if ( - (defaultRootOperationNames as string[]).includes(node.name.value) && - !rootOperationTypeMap[node.name.value] - ) { - return null; - } - return; - }, - // This visitor handles the case where: - // 1) A schema definition or extension is provided by the user - // 2) A field exists that is of a _default_ root operation type. (Query, Mutation, Subscription) - // - // Example: - // - // schema { - // mutation: RootMutation - // } - // - // type RootMutation { - // updateProduct: Query <--- remove this field altogether - // } - FieldDefinition(node) { - if ( - node.type.kind === Kind.NAMED_TYPE && - (defaultRootOperationNames as string[]).includes(node.type.name.value) - ) { - return null; - } - - if ( - node.type.kind === Kind.NON_NULL_TYPE && - node.type.type.kind === Kind.NAMED_TYPE && - (defaultRootOperationNames as string[]).includes( - node.type.type.name.value, - ) - ) { - return null; - } - return; - }, - }); - } - - const schemaWithDefaultRootTypes = visit( - schemaWithoutConflictingDefaultDefinitions, - { - // Schema definitions and extensions are extraneous since we're transforming - // the root operation types to their defaults. - SchemaDefinition() { - return null; - }, - SchemaExtension() { - return null; - }, - ObjectTypeDefinition(node) { - if ( - node.name.value in rootOperationTypeMap || - (defaultRootOperationNames as string[]).includes(node.name.value) - ) { - return { - ...node, - name: { - ...node.name, - value: rootOperationTypeMap[node.name.value] || node.name.value, - }, - kind: Kind.OBJECT_TYPE_EXTENSION, - }; - } - return; - }, - // schema { - // query: RootQuery - // } - // - // extend type RootQuery { <--- update this to `extend type Query` - // ... - // } - ObjectTypeExtension(node) { - if ( - node.name.value in rootOperationTypeMap || - (defaultRootOperationNames as string[]).includes(node.name.value) - ) { - return { - ...node, - name: { - ...node.name, - value: rootOperationTypeMap[node.name.value] || node.name.value, - }, - }; - } - return; - }, - // Corresponding NamedTypes must also make the name switch, in the case that - // they reference a root operation type that we've transformed - // - // schema { - // query: RootQuery - // mutation: RootMutation - // } - // - // type RootQuery { - // ... - // } - // - // type RootMutation { - // updateProduct: RootQuery <--- rename `RootQuery` to `Query` - // } - NamedType(node) { - if (node.name.value in rootOperationTypeMap) { - return { - ...node, - name: { - ...node.name, - value: rootOperationTypeMap[node.name.value], - }, - }; - } - return; - }, - }, - ); - - return schemaWithDefaultRootTypes; -} - -// type definitions with the @extends directive should be treated -// as type extensions. -export function replaceExtendedDefinitionsWithExtensions( - typeDefs: DocumentNode, -) { - const typeDefsWithExtendedTypesReplaced = visit(typeDefs, { - ObjectTypeDefinition: visitor, - InterfaceTypeDefinition: visitor, - }); - - function visitor( - node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - ) { - const isExtensionDefinition = - findDirectivesOnTypeOrField(node, 'extends').length > 0; - - if (!isExtensionDefinition) { - return node; - } - - const filteredDirectives = - node.directives && - node.directives.filter(directive => directive.name.value !== 'extends'); - - return { - ...node, - ...(filteredDirectives && { directives: filteredDirectives }), - kind: defKindToExtKind[node.kind], - }; - } - - return typeDefsWithExtendedTypesReplaced; -} - -// For non-ApolloServer libraries that support federation, this allows a -// library to report the entire schema's SDL rather than an awkward, stripped out -// subset of the schema. Generally there's no need to include the federation -// primitives, but in many cases it's more difficult to exclude them. -// -// This removes the following from a GraphQL Document: -// directives: @external, @key, @requires, @provides, @extends, @skip, @include, @deprecated, @specifiedBy -// scalars: _Any, _FieldSet -// union: _Entity -// object type: _Service -// Query fields: _service, _entities -export function stripCommonPrimitives(document: DocumentNode) { - const typeDefinitionVisitor: VisitFn< - any, - ObjectTypeDefinitionNode | ObjectTypeExtensionNode - > = (node) => { - // Remove the `_entities` and `_service` fields from the `Query` type - if (node.name.value === defaultRootOperationNameLookup.query) { - const filteredFieldDefinitions = node.fields?.filter( - (fieldDefinition) => - !reservedRootFields.includes(fieldDefinition.name.value), - ); - - // If the 'Query' type is now empty just remove it - if (!filteredFieldDefinitions || filteredFieldDefinitions.length === 0) { - return null; - } - - return { - ...node, - fields: filteredFieldDefinitions, - }; - } - - // Remove the _Service type from the document - const isFederationType = node.name.value === '_Service'; - return isFederationType ? null : node; - }; - - return visit(document, { - // Remove all common directive definitions from the document - DirectiveDefinition(node) { - const isCommonDirective = [...federationDirectives, ...specifiedDirectives].some( - (directive) => directive.name === node.name.value, - ); - return isCommonDirective ? null : node; - }, - // Remove all federation scalar definitions from the document - ScalarTypeDefinition(node) { - const isFederationScalar = ['_Any', '_FieldSet'].includes( - node.name.value, - ); - return isFederationScalar ? null : node; - }, - // Remove all federation union definitions from the document - UnionTypeDefinition(node) { - const isFederationUnion = node.name.value === "_Entity"; - return isFederationUnion ? null : node; - }, - ObjectTypeDefinition: typeDefinitionVisitor, - ObjectTypeExtension: typeDefinitionVisitor, - }); -} diff --git a/packages/apollo-federation/src/composition/rules.ts b/packages/apollo-federation/src/composition/rules.ts deleted file mode 100644 index f4ed37e2342..00000000000 --- a/packages/apollo-federation/src/composition/rules.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { specifiedSDLRules } from 'graphql/validation/specifiedRules'; - -/** - * Since this module has overlapping names in the two modules (graphql-js and - * our own, local validation rules) which we are importing from, we - * intentionally are very explicit about the suffixes of imported members here, - * so that the intention is clear. - * - * First, we'll import validation rules from graphql-js which we'll omit and - * replace with our own validation rules. As noted above, we'll use aliases - * with 'FromGraphqlJs' suffixes for clarity. - */ - -import { - UniqueDirectivesPerLocationRule as UniqueDirectivesPerLocationRuleFromGraphqlJs, -} from 'graphql'; -import { - UniqueTypeNames as UniqueTypeNamesFromGraphqlJs, -} from 'graphql/validation/rules/UniqueTypeNames'; -import { - UniqueEnumValueNames as UniqueEnumValueNamesFromGraphqlJs, -} from 'graphql/validation/rules/UniqueEnumValueNames'; -import { - PossibleTypeExtensions as PossibleTypeExtensionsFromGraphqlJs, -} from 'graphql/validation/rules/PossibleTypeExtensions'; -import { - UniqueFieldDefinitionNames as UniqueFieldDefinitionNamesFromGraphqlJs, -} from 'graphql/validation/rules/UniqueFieldDefinitionNames'; - -/** - * Then, we'll import our own validation rules to take the place of those that - * we'll be customizing, taking care to alias them all to the same name with - * "FromComposition" suffixes. - */ -import { - UniqueTypeNamesWithFields as UniqueTypeNamesWithFieldsFromComposition, - MatchingEnums as MatchingEnumsFromComposition, - PossibleTypeExtensions as PossibleTypeExtensionsFromComposition, - UniqueFieldDefinitionNames as UniqueFieldDefinitionsNamesFromComposition, - UniqueUnionTypes as UniqueUnionTypesFromComposition, - } from './validate/sdl'; - -const omit = [ - UniqueDirectivesPerLocationRuleFromGraphqlJs, - UniqueTypeNamesFromGraphqlJs, - UniqueEnumValueNamesFromGraphqlJs, - PossibleTypeExtensionsFromGraphqlJs, - UniqueFieldDefinitionNamesFromGraphqlJs, -]; - -export const compositionRules = specifiedSDLRules - .filter(rule => !omit.includes(rule)) - .concat([ - UniqueFieldDefinitionsNamesFromComposition, - UniqueTypeNamesWithFieldsFromComposition, - MatchingEnumsFromComposition, - UniqueUnionTypesFromComposition, - PossibleTypeExtensionsFromComposition, - ]); diff --git a/packages/apollo-federation/src/composition/types.ts b/packages/apollo-federation/src/composition/types.ts deleted file mode 100644 index 182ed0d5b9b..00000000000 --- a/packages/apollo-federation/src/composition/types.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - SelectionNode, - DocumentNode, - FieldDefinitionNode, - DirectiveDefinitionNode, -} from 'graphql'; - -export type Maybe = null | undefined | T; - -export type ServiceName = string | null; - -export type DefaultRootOperationTypeName = - | 'Query' - | 'Mutation' - | 'Subscription'; - -export interface ExternalFieldDefinition { - field: FieldDefinitionNode; - parentTypeName: string; - serviceName: string; -} - -export interface ServiceNameToKeyDirectivesMap { - [serviceName: string]: ReadonlyArray[]; -} - -export interface FederationType { - serviceName?: ServiceName; - keys?: ServiceNameToKeyDirectivesMap; - externals?: { - [serviceName: string]: ExternalFieldDefinition[]; - }; - isValueType?: boolean; -} - -export interface FederationField { - serviceName?: ServiceName; - requires?: ReadonlyArray; - provides?: ReadonlyArray; - belongsToValueType?: boolean; -} - -export interface FederationDirective { - directiveDefinitions: { - [serviceName: string]: DirectiveDefinitionNode; - } -} - -export interface ServiceDefinition { - typeDefs: DocumentNode; - name: string; - url?: string; -} - -declare module 'graphql/language/ast' { - interface UnionTypeDefinitionNode { - serviceName?: string | null; - } - interface UnionTypeExtensionNode { - serviceName?: string | null; - } - - interface EnumTypeDefinitionNode { - serviceName?: string | null; - } - - interface EnumTypeExtensionNode { - serviceName?: string | null; - } - - interface ScalarTypeDefinitionNode { - serviceName?: string | null; - } - - interface ScalarTypeExtensionNode { - serviceName?: string | null; - } - - interface ObjectTypeDefinitionNode { - serviceName?: string | null; - } - - interface ObjectTypeExtensionNode { - serviceName?: string | null; - } - - interface InterfaceTypeDefinitionNode { - serviceName?: string | null; - } - - interface InterfaceTypeExtensionNode { - serviceName?: string | null; - } - - interface InputObjectTypeDefinitionNode { - serviceName?: string | null; - } - - interface InputObjectTypeExtensionNode { - serviceName?: string | null; - } -} diff --git a/packages/apollo-federation/src/composition/utils.ts b/packages/apollo-federation/src/composition/utils.ts deleted file mode 100644 index 2c0a0ea9e09..00000000000 --- a/packages/apollo-federation/src/composition/utils.ts +++ /dev/null @@ -1,583 +0,0 @@ -import 'apollo-server-env'; -import { - InterfaceTypeExtensionNode, - FieldDefinitionNode, - Kind, - StringValueNode, - parse, - OperationDefinitionNode, - NameNode, - DocumentNode, - visit, - ObjectTypeExtensionNode, - DirectiveNode, - GraphQLNamedType, - GraphQLError, - GraphQLSchema, - isObjectType, - GraphQLObjectType, - getNamedType, - GraphQLField, - SelectionNode, - isEqualType, - FieldNode, - TypeDefinitionNode, - InputValueDefinitionNode, - TypeExtensionNode, - BREAK, - print, - ASTNode, - DirectiveDefinitionNode, - GraphQLDirective, - OperationTypeNode, - isDirective, - isNamedType, -} from 'graphql'; -import { - ExternalFieldDefinition, - DefaultRootOperationTypeName, - Maybe, - FederationType, - FederationDirective, - FederationField, -} from './types'; -import federationDirectives from '../directives'; - -export function isStringValueNode(node: any): node is StringValueNode { - return node.kind === Kind.STRING; -} - -// Create a map of { fieldName: serviceName } for each field. -export function mapFieldNamesToServiceName( - fields: ReadonlyArray, - serviceName: string, -) { - return fields.reduce((prev, next) => { - prev[next.name.value] = serviceName; - return prev; - }, Object.create(null)); -} - -export function findDirectivesOnTypeOrField( - node: Maybe, - directiveName: string, -) { - return node && node.directives - ? node.directives.filter( - directive => directive.name.value === directiveName, - ) - : []; -} - -export function stripExternalFieldsFromTypeDefs( - typeDefs: DocumentNode, - serviceName: string, -): { - typeDefsWithoutExternalFields: DocumentNode; - strippedFields: ExternalFieldDefinition[]; -} { - const strippedFields: ExternalFieldDefinition[] = []; - - const typeDefsWithoutExternalFields = visit(typeDefs, { - ObjectTypeExtension: removeExternalFieldsFromExtensionVisitor( - strippedFields, - serviceName, - ), - InterfaceTypeExtension: removeExternalFieldsFromExtensionVisitor( - strippedFields, - serviceName, - ), - }) as DocumentNode; - - return { typeDefsWithoutExternalFields, strippedFields }; -} - -export function stripTypeSystemDirectivesFromTypeDefs(typeDefs: DocumentNode) { - const typeDefsWithoutTypeSystemDirectives = visit(typeDefs, { - Directive(node) { - // The `deprecated` directive is an exceptional case that we want to leave in - if (node.name.value === 'deprecated' || node.name.value === 'specifiedBy') return; - - const isFederationDirective = federationDirectives.some( - ({ name }) => name === node.name.value, - ); - // Returning `null` to a visit will cause it to be removed from the tree. - return isFederationDirective ? undefined : null; - }, - }) as DocumentNode; - - return typeDefsWithoutTypeSystemDirectives; -} - -/** - * Returns a closure that strips fields marked with `@external` and adds them - * to an array. - * @param collector - * @param serviceName - */ -function removeExternalFieldsFromExtensionVisitor< - T extends InterfaceTypeExtensionNode | ObjectTypeExtensionNode ->(collector: ExternalFieldDefinition[], serviceName: string) { - return (node: T) => { - let fields = node.fields; - if (fields) { - fields = fields.filter(field => { - const externalDirectives = findDirectivesOnTypeOrField( - field, - 'external', - ); - - if (externalDirectives.length > 0) { - collector.push({ - field, - parentTypeName: node.name.value, - serviceName, - }); - return false; - } - return true; - }); - } - return { - ...node, - fields, - }; - }; -} - -export function parseSelections(source: string) { - return (parse(`query { ${source} }`) - .definitions[0] as OperationDefinitionNode).selectionSet.selections; -} - -export function hasMatchingFieldInDirectives({ - directives, - fieldNameToMatch, - namedType, -}: { - directives: DirectiveNode[]; - fieldNameToMatch: String; - namedType: GraphQLNamedType; -}) { - return Boolean( - namedType.astNode && - directives - // for each key directive, get the fields arg - .map(keyDirective => - keyDirective.arguments && - isStringValueNode(keyDirective.arguments[0].value) - ? { - typeName: namedType.astNode!.name.value, - keyArgument: keyDirective.arguments[0].value.value, - } - : null, - ) - // filter out any null/undefined args - .filter(isNotNullOrUndefined) - // flatten all selections of the "fields" arg to a list of fields - .flatMap(selection => parseSelections(selection.keyArgument)) - // find a field that matches the @external field - .some( - field => - field.kind === Kind.FIELD && field.name.value === fieldNameToMatch, - ), - ); -} - -export const logServiceAndType = ( - serviceName: string, - typeName: string, - fieldName?: string, -) => `[${serviceName}] ${typeName}${fieldName ? `.${fieldName} -> ` : ' -> '}`; - -export function logDirective(directiveName: string) { - return `[@${directiveName}] -> `; -} - -// TODO: allow passing of the other args here, rather than just message and code -export function errorWithCode( - code: string, - message: string, - nodes?: ReadonlyArray | ASTNode | undefined, -) { - return new GraphQLError( - message, - nodes, - undefined, - undefined, - undefined, - undefined, - { - code, - }, - ); -} - -export function findTypesContainingFieldWithReturnType( - schema: GraphQLSchema, - node: GraphQLField, -): GraphQLObjectType[] { - const returnType = getNamedType(node.type); - if (!isObjectType(returnType)) return []; - - const containingTypes: GraphQLObjectType[] = []; - const types = schema.getTypeMap(); - for (const selectionSetType of Object.values(types)) { - // Only object types have fields - if (!isObjectType(selectionSetType)) continue; - const allFields = selectionSetType.getFields(); - - // only push types that have a field which returns the returnType - Object.values(allFields).forEach(field => { - const fieldReturnType = getNamedType(field.type); - if (fieldReturnType === returnType) { - containingTypes.push(fieldReturnType); - } - }); - } - return containingTypes; -} - -/** - * Used for finding a field on the `schema` that returns `typeToFind` - * - * Used in validation of external directives to find uses of a field in a - * `@provides` on another type. - */ -export function findFieldsThatReturnType({ - schema, - typeToFind, -}: { - schema: GraphQLSchema; - typeToFind: GraphQLNamedType; -}): GraphQLField[] { - if (!isObjectType(typeToFind)) return []; - - const fieldsThatReturnType: GraphQLField[] = []; - const types = schema.getTypeMap(); - - for (const selectionSetType of Object.values(types)) { - // for our purposes, only object types have fields that we care about. - if (!isObjectType(selectionSetType)) continue; - - const fieldsOnNamedType = selectionSetType.getFields(); - - // push fields that have return `typeToFind` - Object.values(fieldsOnNamedType).forEach(field => { - const fieldReturnType = getNamedType(field.type); - if (fieldReturnType === typeToFind) { - fieldsThatReturnType.push(field); - } - }); - } - return fieldsThatReturnType; -} - -/** - * Searches recursively to see if a selection set includes references to - * `typeToFind.fieldToFind`. - * - * Used in validation of external fields to find where/if a field is referenced - * in a nested selection set for `@requires` - * - * For every selection, look at the root of the selection's type. - * 1. If it's the type we're looking for, check its fields. - * Return true if field matches. Skip to step 3 if not - * 2. If it's not the type we're looking for, skip to step 3 - * 3. Get the return type for each subselection and run this function on the subselection. - */ -export function selectionIncludesField({ - selections, - selectionSetType, - typeToFind, - fieldToFind, -}: { - selections: readonly SelectionNode[]; - selectionSetType: GraphQLObjectType; // type which applies to `selections` - typeToFind: GraphQLObjectType; // type where the `@external` lives - fieldToFind: string; -}): boolean { - for (const selection of selections as FieldNode[]) { - const selectionName: string = selection.name.value; - - // if the selected field matches the fieldname we're looking for, - // and its type is correct, we're done. Return true; - if ( - selectionName === fieldToFind && - isEqualType(selectionSetType, typeToFind) - ) - return true; - - // if the field selection has a subselection, check each field recursively - - // check to make sure the parent type contains the field - const typeIncludesField = - selectionName && - Object.keys(selectionSetType.getFields()).includes(selectionName); - if (!selectionName || !typeIncludesField) continue; - - // get the return type of the selection - const returnType = getNamedType( - selectionSetType.getFields()[selectionName].type, - ); - if (!returnType || !isObjectType(returnType)) continue; - const subselections = - selection.selectionSet && selection.selectionSet.selections; - - // using the return type of a given selection and all the subselections, - // recursively search for matching selections. typeToFind and fieldToFind - // stay the same - if (subselections) { - const selectionDoesIncludeField = selectionIncludesField({ - selectionSetType: returnType, - selections: subselections, - typeToFind, - fieldToFind, - }); - if (selectionDoesIncludeField) return true; - } - } - return false; -} - -/** - * Returns true if a @key directive is found on the type node - * - * @param node TypeDefinitionNode | TypeExtensionNode - * @returns boolean - */ -export function isTypeNodeAnEntity( - node: TypeDefinitionNode | TypeExtensionNode, -) { - let isEntity = false; - - visit(node, { - Directive(directive) { - if (directive.name.value === 'key') { - isEntity = true; - return BREAK; - } - }, - }); - - return isEntity; -} - -/** - * Diff two type nodes. This returns an object consisting of useful properties and their differences - * - name: An array of length 0 or 2. If their type names are different, they will be added to the array. - * (['Product', 'Product']) - * - fields: An entry in the fields object can mean two things: - * 1) a field was found on one type, but not the other (fieldName: ['String!']) - * 2) a common field was found, but their types differ (fieldName: ['String!', 'Int!']) - * - kind: An array of length 0 or 2. If their kinds are different, they will be added to the array. - * (['InputObjectTypeDefinition', 'InterfaceTypeDefinition']) - * - * @param firstNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode - * @param secondNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode - */ -export function diffTypeNodes( - firstNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, - secondNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, -) { - const fieldsDiff: { - [fieldName: string]: string[]; - } = Object.create(null); - - const unionTypesDiff: { - [typeName: string]: boolean; - } = Object.create(null); - - const locationsDiff: Set = new Set(); - - const argumentsDiff: { - [argumentName: string]: string[]; - } = Object.create(null); - - const document: DocumentNode = { - kind: Kind.DOCUMENT, - definitions: [firstNode, secondNode], - }; - - function fieldVisitor(node: FieldDefinitionNode | InputValueDefinitionNode) { - const fieldName = node.name.value; - - const type = print(node.type); - - if (!fieldsDiff[fieldName]) { - fieldsDiff[fieldName] = [type]; - return; - } - - // If we've seen this field twice and the types are the same, remove this - // field from the diff result - const fieldTypes = fieldsDiff[fieldName]; - if (fieldTypes[0] === type) { - delete fieldsDiff[fieldName]; - } else { - fieldTypes.push(type); - } - } - - visit(document, { - FieldDefinition: fieldVisitor, - InputValueDefinition: fieldVisitor, - UnionTypeDefinition(node) { - if (!node.types) return BREAK; - for (const namedTypeNode of node.types) { - const name = namedTypeNode.name.value; - if (unionTypesDiff[name]) { - delete unionTypesDiff[name]; - } else { - unionTypesDiff[name] = true; - } - } - }, - DirectiveDefinition(node) { - node.locations.forEach(location => { - const locationName = location.value; - // If a location already exists in the Set, then we've seen it once. - // This means we can remove it from the final diff, since both directives - // have this location in common. - if (locationsDiff.has(locationName)) { - locationsDiff.delete(locationName); - } else { - locationsDiff.add(locationName); - } - }); - - if (!node.arguments) return; - - // Arguments must have the same name and type. As matches are found, they - // are deleted from the diff. Anything left in the diff after looping - // represents a discrepancy between the two sets of arguments. - node.arguments.forEach(argument => { - const argumentName = argument.name.value; - const printedType = print(argument.type); - if (argumentsDiff[argumentName]) { - if (printedType === argumentsDiff[argumentName][0]) { - // If the existing entry is equal to printedType, it means there's no - // diff, so we can remove the entry from the diff object - delete argumentsDiff[argumentName]; - } else { - argumentsDiff[argumentName].push(printedType); - } - } else { - argumentsDiff[argumentName] = [printedType]; - } - }); - }, - }); - - const typeNameDiff = - firstNode.name.value === secondNode.name.value - ? [] - : [firstNode.name.value, secondNode.name.value]; - - const kindDiff = - firstNode.kind === secondNode.kind ? [] : [firstNode.kind, secondNode.kind]; - - return { - name: typeNameDiff, - kind: kindDiff, - fields: fieldsDiff, - unionTypes: unionTypesDiff, - locations: Array.from(locationsDiff), - args: argumentsDiff, - }; -} - -/** - * A common implementation of diffTypeNodes to ensure two type nodes are equivalent - * - * @param firstNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode - * @param secondNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode - */ -export function typeNodesAreEquivalent( - firstNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, - secondNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, -) { - const { name, kind, fields, unionTypes, locations, args } = diffTypeNodes( - firstNode, - secondNode, - ); - - return ( - name.length === 0 && - kind.length === 0 && - Object.keys(fields).length === 0 && - Object.keys(unionTypes).length === 0 && - locations.length === 0 && - Object.keys(args).length === 0 - ); -} - -/** - * A map of `Kind`s from their definition to their respective extensions - */ -export const defKindToExtKind: { [kind: string]: string } = { - [Kind.SCALAR_TYPE_DEFINITION]: Kind.SCALAR_TYPE_EXTENSION, - [Kind.OBJECT_TYPE_DEFINITION]: Kind.OBJECT_TYPE_EXTENSION, - [Kind.INTERFACE_TYPE_DEFINITION]: Kind.INTERFACE_TYPE_EXTENSION, - [Kind.UNION_TYPE_DEFINITION]: Kind.UNION_TYPE_EXTENSION, - [Kind.ENUM_TYPE_DEFINITION]: Kind.ENUM_TYPE_EXTENSION, - [Kind.INPUT_OBJECT_TYPE_DEFINITION]: Kind.INPUT_OBJECT_TYPE_EXTENSION, -}; - -// Transform an object's values via a callback function -export function mapValues( - object: Record, - callback: (value: T) => U, -): Record { - const result: Record = Object.create(null); - - for (const [key, value] of Object.entries(object)) { - result[key] = callback(value); - } - - return result; -} - -export function isNotNullOrUndefined( - value: T | null | undefined, -): value is T { - return value !== null && typeof value !== 'undefined'; -} - -export const executableDirectiveLocations = [ - 'QUERY', - 'MUTATION', - 'SUBSCRIPTION', - 'FIELD', - 'FRAGMENT_DEFINITION', - 'FRAGMENT_SPREAD', - 'INLINE_FRAGMENT', - 'VARIABLE_DEFINITION', -]; - -export function isFederationDirective(directive: GraphQLDirective): boolean { - return federationDirectives.some(({ name }) => name === directive.name); -} - -export const reservedRootFields = ['_service', '_entities']; - -// Map of OperationTypeNode to its respective default root operation type name -export const defaultRootOperationNameLookup: { - [node in OperationTypeNode]: DefaultRootOperationTypeName; -} = { - query: 'Query', - mutation: 'Mutation', - subscription: 'Subscription', -}; - -// This function is overloaded for 3 different input types. Each input type -// maps to a particular return type, hence the overload. -export function getFederationMetadata(obj: GraphQLNamedType): FederationType | undefined; -export function getFederationMetadata(obj: GraphQLField): FederationField | undefined; -export function getFederationMetadata(obj: GraphQLDirective): FederationDirective | undefined; -export function getFederationMetadata(obj: any) { - if (typeof obj === "undefined") return undefined; - else if (isNamedType(obj)) return obj.extensions?.federation as FederationType | undefined; - else if (isDirective(obj)) return obj.extensions?.federation as FederationDirective | undefined; - else return obj.extensions?.federation as FederationField | undefined; -} diff --git a/packages/apollo-federation/src/composition/validate/__tests__/tsconfig.json b/packages/apollo-federation/src/composition/validate/__tests__/tsconfig.json deleted file mode 100644 index 9b9bff3d5d8..00000000000 --- a/packages/apollo-federation/src/composition/validate/__tests__/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [{ "path": "../../../../" }] -} diff --git a/packages/apollo-federation/src/composition/validate/index.ts b/packages/apollo-federation/src/composition/validate/index.ts deleted file mode 100644 index fc9385c355c..00000000000 --- a/packages/apollo-federation/src/composition/validate/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { GraphQLSchema, GraphQLError, validateSchema } from 'graphql'; -import { ServiceDefinition } from '../types'; - -// import validators -import * as preNormalizationRules from './preNormalization'; -import * as preCompositionRules from './preComposition'; -import * as postCompositionRules from './postComposition'; - -const preNormalizationValidators = Object.values(preNormalizationRules); - -export function validateServicesBeforeNormalization( - services: ServiceDefinition[], -) { - const errors: GraphQLError[] = []; - - for (const serviceDefinition of services) { - for (const validator of preNormalizationValidators) { - errors.push(...validator(serviceDefinition)); - } - } - - return errors; -} - -const preCompositionValidators = Object.values(preCompositionRules); - -export const validateServicesBeforeComposition = ( - services: ServiceDefinition[], -) => { - const warningsOrErrors: GraphQLError[] = []; - - for (const serviceDefinition of services) { - for (const validator of preCompositionValidators) { - warningsOrErrors.push(...validator(serviceDefinition)); - } - } - - return warningsOrErrors; -}; - -const postCompositionValidators = Object.values(postCompositionRules); - -export const validateComposedSchema = ({ - schema, - serviceList, -}: { - schema: GraphQLSchema; - serviceList: ServiceDefinition[]; -}): GraphQLError[] => { - const warningsOrErrors: GraphQLError[] = []; - - // https://github.com/graphql/graphql-js/blob/4b55f10f16cc77302613e8ad67440259c68633df/src/type/validate.js#L56 - warningsOrErrors.push(...validateSchema(schema)); - for (const validator of postCompositionValidators) { - warningsOrErrors.push(...validator({ schema, serviceList })); - } - - return warningsOrErrors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts deleted file mode 100644 index 695e24efbbe..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { executableDirectivesIdentical } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('executableDirectivesIdentical', () => { - it('throws no errors when custom, executable directives are defined identically every service', () => { - const serviceA = { - typeDefs: gql` - directive @stream on FIELD - directive @instrument(tag: String!) on FIELD - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - directive @stream on FIELD - directive @instrument(tag: String!) on FIELD - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const errors = executableDirectivesIdentical({ schema, serviceList }); - expect(errors).toHaveLength(0); - }); - - it('throws no errors when directives (excluding their TypeSystemDirectiveLocations) are identical for every service', () => { - const serviceA = { - typeDefs: gql` - directive @stream on FIELD - directive @instrument(tag: String!) on FIELD | FIELD_DEFINITION - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - directive @stream on FIELD - directive @instrument(tag: String!) on FIELD - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const errors = executableDirectivesIdentical({ schema, serviceList }); - expect(errors).toHaveLength(0); - }); - - it("throws errors when custom, executable directives aren't defined with the same locations in every service", () => { - const serviceA = { - typeDefs: gql` - directive @stream on FIELD - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - directive @stream on FIELD | QUERY - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - directive @stream on INLINE_FRAGMENT - `, - name: 'serviceC', - }; - - const serviceList = [serviceA, serviceB, serviceC]; - const { schema } = composeServices(serviceList); - const errors = executableDirectivesIdentical({ schema, serviceList }); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", - "message": "[@stream] -> custom directives must be defined identically across all services. See below for a list of current implementations: - serviceA: directive @stream on FIELD - serviceB: directive @stream on FIELD | QUERY - serviceC: directive @stream on INLINE_FRAGMENT", - }, - ] - `); - }); - - it("throws errors when custom, executable directives aren't defined with the same arguments in every service", () => { - const serviceA = { - typeDefs: gql` - directive @instrument(tag: String!) on FIELD - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - directive @instrument(tag: Boolean) on FIELD - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const errors = executableDirectivesIdentical({ schema, serviceList }); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", - "message": "[@instrument] -> custom directives must be defined identically across all services. See below for a list of current implementations: - serviceA: directive @instrument(tag: String!) on FIELD - serviceB: directive @instrument(tag: Boolean) on FIELD", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts deleted file mode 100644 index 3e090dcab97..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { executableDirectivesInAllServices } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('executableDirectivesInAllServices', () => { - it('throws no errors when custom, executable directives are defined in every service', () => { - const serviceA = { - typeDefs: gql` - directive @stream on FIELD - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - directive @stream on FIELD - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const errors = executableDirectivesInAllServices({ schema, serviceList }); - expect(errors).toHaveLength(0); - }); - - it("throws no errors when type system directives aren't defined in every service", () => { - const serviceA = { - typeDefs: gql` - directive @stream on FIELD - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - directive @stream on FIELD - # This directive is ignored by composition and therefore post-composition validators - directive @ignored on FIELD_DEFINITION - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const errors = executableDirectivesInAllServices({ schema, serviceList }); - expect(errors).toHaveLength(0); - }); - - it("throws errors when custom, executable directives aren't defined in every service", () => { - const serviceA = { - typeDefs: gql` - directive @stream on FIELD - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Query { - thing: String - } - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - extend type Query { - otherThing: String - } - `, - name: 'serviceC', - }; - - const serviceList = [serviceA, serviceB, serviceC]; - const { schema } = composeServices(serviceList); - const errors = executableDirectivesInAllServices({ schema, serviceList }); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXECUTABLE_DIRECTIVES_IN_ALL_SERVICES", - "message": "[@stream] -> Custom directives must be implemented in every service. The following services do not implement the @stream directive: serviceB, serviceC.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts deleted file mode 100644 index 5f9f086bbca..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { externalMissingOnBase as validateExternalMissingOnBase } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('externalMissingOnBase', () => { - it('warns when an @external field does not have a matching field on the base type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - id: String! @external - price: Int! @requires(fields: "sku id") - } - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - id: String! - test: Int @external - } - `, - name: 'serviceC', - }; - - const serviceList = [serviceA, serviceB, serviceC]; - const { schema } = composeServices([serviceA, serviceB, serviceC]); - const warnings = validateExternalMissingOnBase({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTERNAL_MISSING_ON_BASE", - "message": "[serviceB] Product.id -> marked @external but id was defined in serviceC, not in the service that owns Product (serviceA)", - }, - Object { - "code": "EXTERNAL_MISSING_ON_BASE", - "message": "[serviceC] Product.test -> marked @external but test is not defined on the base service of Product (serviceA)", - }, - ] - `); - }); - - it("warns when an @external field isn't defined anywhere else", () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - specialId: String! @external - id: String! @requires(fields: "specialId") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalMissingOnBase({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTERNAL_MISSING_ON_BASE", - "message": "[serviceB] Product.specialId -> marked @external but specialId is not defined on the base service of Product (serviceA)", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts deleted file mode 100644 index ce59124e2f9..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import gql from 'graphql-tag'; -import { externalTypeMismatch as validateExternalTypeMismatch } from '../'; -import { composeServices } from '../../../compose'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('validateExternalDirectivesOnSchema', () => { - it('warns when the type of an @external field doesnt match the base', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalTypeMismatch({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTERNAL_TYPE_MISMATCH", - "message": "[serviceB] Product.sku -> Type \`String\` does not match the type of the original field in serviceA (\`String!\`)", - }, - ] - `); - }); - - it("warns when an @external field's type does not exist in the composed schema", () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: NonExistentType! @external - id: String! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalTypeMismatch({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTERNAL_TYPE_MISMATCH", - "message": "[serviceB] Product.sku -> the type of the @external field does not exist in the resulting composed schema", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalUnused.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalUnused.test.ts deleted file mode 100644 index e3156c01774..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/externalUnused.test.ts +++ /dev/null @@ -1,380 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { externalUnused as validateExternalUnused } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('externalUnused', () => { - it('warns when there is an unused @external field', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "id") { - sku: String! - upc: String! - id: ID! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - id: ID! @external - price: Int! @requires(fields: "id") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTERNAL_UNUSED", - "message": "[serviceB] Product.sku -> is marked as @external but is not used by a @requires, @key, or @provides directive.", - }, - ] - `); - }); - - it('does not warn when @external is selected by a @key', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - price: Float! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it('does not warn when @external is selected by a @requires', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it('does not warn when @external is selected by a @provides', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - id: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - price: Int! @provides(fields: "id") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it('does not warn when @external is selected by a @provides used from another type', () => { - const serviceA = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - username: String - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Review { - author: User @provides(fields: "username") - } - - extend type User @key(fields: "id") { - username: String @external - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it.todo( - 'does not error when @provides selects an external field in a subselection', - ); - - it.todo('errors when there is an invalid selection in @requires'); - - it('does not warn when @external is selected by a @requires used from another type', () => { - const serviceA = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - username: String - } - - type AccountRoles { - canRead: Boolean - canWrite: Boolean - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Review { - author: User - } - - extend type User @key(fields: "id") { - roles: AccountRoles! - isAdmin: Boolean! @requires(fields: "roles { canWrite }") - } - - # Externals -- only referenced by the @requires on User.isAdmin - extend type AccountRoles { - canWrite: Boolean @external - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it('does not warn when @external is selected by a @requires in a deep subselection', () => { - const serviceA = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - username: String - } - - type AccountRoles { - canRead: Group - canWrite: Group - } - - type Group { - id: ID! - name: String - members: [User] - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Review { - author: User - } - - extend type User @key(fields: "id") { - id: ID! @external - roles: AccountRoles! - username: String @external - isAdmin: Boolean! - @requires( - fields: """ - roles { - canWrite { - members { - username - } - } - canRead { - members { - username - } - } - } - """ - ) - } - - # Externals -- only referenced by the @requires on User.isAdmin - extend type AccountRoles { - canWrite: Group @external - canRead: Group @external - } - - extend type Group { - members: [User] @external - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it('does not warn when @external is used on type with multiple @key directives', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "upc") @key(fields: "sku") { - upc: String - sku: String - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "upc") { - upc: String @external - } - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String @external - } - `, - name: 'serviceC', - }; - - const serviceList = [serviceA, serviceB, serviceC]; - const { schema } = composeServices(serviceList); - const warnings = validateExternalUnused({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it('does not error when @external is used on a field of a concrete type that implements a shared field of an implemented interface', () => { - const serviceA = { - typeDefs: gql` - type Car implements Vehicle @key(fields: "id") { - id: ID! - speed: Int - } - interface Vehicle { - id: ID! - speed: Int - } - `, - name: 'serviceA', - }; - const serviceB = { - typeDefs: gql` - extend type Car implements Vehicle @key(fields: "id") { - id: ID! @external - speed: Int @external - } - interface Vehicle { - id: ID! - speed: Int - } - `, - name: 'serviceB', - }; - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const errors = validateExternalUnused({ schema, serviceList }); - expect(errors).toHaveLength(0); - }); - - it('does error when @external is used on a field of a concrete type is not shared by its implemented interface', () => { - const serviceA = { - typeDefs: gql` - type Car implements Vehicle @key(fields: "id") { - id: ID! - speed: Int - wheelSize: Int - } - interface Vehicle { - id: ID! - speed: Int - } - `, - name: 'serviceA', - }; - const serviceB = { - typeDefs: gql` - extend type Car implements Vehicle @key(fields: "id") { - id: ID! @external - speed: Int @external - wheelSize: Int @external - } - interface Vehicle { - id: ID! - speed: Int - } - `, - name: 'serviceB', - }; - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const errors = validateExternalUnused({ schema, serviceList }); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTERNAL_UNUSED", - "message": "[serviceB] Car.wheelSize -> is marked as @external but is not used by a @requires, @key, or @provides directive.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts deleted file mode 100644 index 5b32294034b..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { keyFieldsMissingOnBase as validateKeyFieldsMissingOnBase } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('keyFieldsMissingOnBase', () => { - it('returns no warnings with proper @key usage', () => { - const serviceA = { - // FIXME: add second key "upc" when duplicate directives are supported - // i.e. @key(fields: "sku") @key(fields: "upc") - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateKeyFieldsMissingOnBase({ schema, serviceList }); - expect(warnings).toHaveLength(0); - }); - - it('warns if @key references a field added by another service', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku uid") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - uid: String! - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateKeyFieldsMissingOnBase({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "KEY_FIELDS_MISSING_ON_BASE", - "message": "[serviceA] Product -> A @key selects uid, but Product.uid was either created or overwritten by serviceB, not serviceA", - }, - ] - `); - }); - - // FIXME: shouldn't composition _allow_ this with a warning? - // right now, it errors during composition - xit('warns if @key references a field that was overwritten', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: ID! # overwritten from base service - weight: Float! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateKeyFieldsMissingOnBase({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts deleted file mode 100644 index 2d77ff89f4b..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { keyFieldsSelectInvalidType as validateKeyFieldsSelectInvalidType } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('keyFieldsSelectInvalidType', () => { - it('returns no warnings with proper @key usage', () => { - const serviceA = { - // FIXME: add second key "upc" when duplicate directives are supported - // i.e. @key(fields: "sku") @key(fields: "upc") - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateKeyFieldsSelectInvalidType({ - schema, - serviceList, - }); - expect(warnings).toHaveLength(0); - }); - - it('warns if @key references fields of an interface type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "featuredItem") { - featuredItem: Node! - sku: String! - } - - interface Node { - id: ID! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - price: Int! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateKeyFieldsSelectInvalidType({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "KEY_FIELDS_SELECT_INVALID_TYPE", - "message": "[serviceA] Product -> A @key selects Product.featuredItem, which is an interface type. Keys cannot select interfaces.", - }, - ] - `); - }); - - it('warns if @key references fields of a union type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "price") { - sku: String! - price: Numeric! - } - - union Numeric = Float | Int - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - sku: String! @external - name: String! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateKeyFieldsSelectInvalidType({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "KEY_FIELDS_SELECT_INVALID_TYPE", - "message": "[serviceA] Product -> A @key selects Product.price, which is a union type. Keys cannot select union types.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts deleted file mode 100644 index 5c2854f64e3..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { keysMatchBaseService as validateKeysMatchBaseService } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('keysMatchBaseService', () => { - it('returns no errors with proper @key usage', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - price: Int! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const validationErrors = validateKeysMatchBaseService({ - schema, - serviceList, - }); - expect(validationErrors).toHaveLength(0); - }); - - it('requires a @key to be specified on the originating type', () => { - const serviceA = { - typeDefs: gql` - type Product { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - price: Int! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const validationErrors = validateKeysMatchBaseService({ - schema, - serviceList, - }); - expect(validationErrors).toHaveLength(1); - expect(validationErrors[0]).toMatchInlineSnapshot(` - Object { - "code": "KEY_MISSING_ON_BASE", - "message": "[serviceA] Product -> appears to be an entity but no @key directives are specified on the originating type.", - } - `); - }); - - it('requires extending services to use a @key specified by the originating type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku upc") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - price: Int! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const validationErrors = validateKeysMatchBaseService({ - schema, - serviceList, - }); - expect(validationErrors).toHaveLength(1); - expect(validationErrors[0]).toMatchInlineSnapshot(` - Object { - "code": "KEY_NOT_SPECIFIED", - "message": "[serviceB] Product -> extends from serviceA but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are: - @key(fields: \\"sku upc\\")", - } - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts deleted file mode 100644 index 0eed1d298b1..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { providesFieldsMissingExternal as validateProdivesFieldsMissingExternal } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('providesFieldsMissingExternal', () => { - it('does not warn with proper @provides usage', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - id: ID! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - username: String - } - `, - name: 'serviceB', - }; - - const serviceC = { - typeDefs: gql` - type Review @key(fields: "id") { - id: ID! - product: Product @provides(fields: "id") - author: User @provides(fields: "username") - } - - extend type Product @key(fields: "sku") { - sku: String! @external - id: ID! @external - price: Int! - } - - extend type User @key(fields: "id") { - id: ID! @external - username: String @external - } - `, - name: 'serviceC', - }; - - const serviceList = [serviceA, serviceB, serviceC]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toEqual([]); - const warnings = validateProdivesFieldsMissingExternal({ - schema, - serviceList, - }); - expect(warnings).toEqual([]); - }); - - it('warns when there is a @provides with no matching @external field', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - id: ID! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Review @key(fields: "id") { - id: ID! - product: Product @provides(fields: "id") - } - - extend type Product @key(fields: "sku") { - sku: String! @external - price: Int! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toEqual([]); - const warnings = validateProdivesFieldsMissingExternal({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_FIELDS_MISSING_EXTERNAL", - "message": "[serviceB] Review.product -> provides the field \`id\` and requires Product.id to be marked as @external.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts deleted file mode 100644 index 364b491aad8..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { providesFieldsSelectInvalidType as validateprovidesFieldsSelectInvalidType } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('providesFieldsSelectInvalidType', () => { - it('returns no warnings with proper @provides usage', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - upc: String! @external - price: Int! @provides(fields: "upc") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateprovidesFieldsSelectInvalidType({ - schema, - serviceList, - }); - expect(warnings).toHaveLength(0); - }); - - it('warns if @provides references fields of a list type', () => { - const serviceA = { - typeDefs: gql` - type Review @key(fields: "id") { - id: ID! - author: User @provides(fields: "wishLists") - } - - extend type User @key(fields: "id") { - id: ID! @external - wishLists: [WishList] @external - } - - extend type WishList @key(fields: "id") { - id: ID! @external - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - wishLists: [WishList] - } - - type WishList @key(fields: "id") { - id: ID! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateprovidesFieldsSelectInvalidType({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", - "message": "[serviceA] Review.author -> A @provides selects User.wishLists, which is a list type. A field cannot @provide lists.", - }, - ] - `); - }); - - it('warns if @provides references fields of an interface type', () => { - const serviceA = { - typeDefs: gql` - type Review @key(fields: "id") { - id: ID! - author: User @provides(fields: "account") - } - - extend type User @key(fields: "id") { - id: ID! @external - account: Account @external - } - - extend interface Account { - username: String @external - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - account: Account - } - - interface Account { - username: String - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateprovidesFieldsSelectInvalidType({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", - "message": "[serviceA] Review.author -> A @provides selects User.account, which is an interface type. A field cannot @provide interfaces.", - }, - ] - `); - }); - - it('warns if @provides references fields of a union type', () => { - const serviceA = { - typeDefs: gql` - type Review @key(fields: "id") { - id: ID! - author: User @provides(fields: "account") - } - - extend type User @key(fields: "id") { - id: ID! @external - account: Account @external - } - - extend union Account = PasswordAccount | SMSAccount - - extend type PasswordAccount @key(fields: "email") { - email: String! @external - } - - extend type SMSAccount @key(fields: "phone") { - phone: String! @external - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - account: Account - } - - union Account = PasswordAccount | SMSAccount - - type PasswordAccount @key(fields: "email") { - email: String! - } - - type SMSAccount @key(fields: "phone") { - phone: String! - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema, errors } = composeServices(serviceList); - expect(errors).toHaveLength(0); - - const warnings = validateprovidesFieldsSelectInvalidType({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", - "message": "[serviceA] Review.author -> A @provides selects User.account, which is a union type. A field cannot @provide union types.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts deleted file mode 100644 index 1a87fc985f0..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { providesNotOnEntity as validateProvidesNotOnEntity } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('providesNotOnEntity', () => { - it('does not warn when @provides used on an entity', () => { - const serviceA = { - typeDefs: gql` - type LineItem @key(fields: "sku") { - sku: String! - quantity: Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product { - lineItem: LineItem @provides(fields: "quantity") - lineItemNonNull: LineItem! @provides(fields: "quantity") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateProvidesNotOnEntity({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(`Array []`); - }); - - it('does not warn when @provides used on a list of entity', () => { - const serviceA = { - typeDefs: gql` - type LineItem @key(fields: "sku") { - sku: String! - quantity: Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Product { - lineItems: [LineItem] @provides(fields: "quantity") - lineItemsNonNull: [LineItem]! @provides(fields: "quantity") - nonNullLineItems: [LineItem!] @provides(fields: "quantity") - nonNullLineItemsNonNull: [LineItem!]! @provides(fields: "quantity") - deep: [[LineItem!]!]! @provides(fields: "quantity") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateProvidesNotOnEntity({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(`Array []`); - }); - - it('does not warn when @provides used on an entity of a child type', () => { - const serviceA = { - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - username: String - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Review { - author: User @provides(fields: "username") - } - - type User { - username: String @external - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateProvidesNotOnEntity({ schema, serviceList }); - expect(warnings).toEqual([]); - }); - - it('warns when there is a @provides on a type that is not an entity', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - id: ID! - } - - type LineItem { - sku: String! - quantity: Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - lineItem: LineItem @provides(fields: "quantity") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateProvidesNotOnEntity({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_NOT_ON_ENTITY", - "message": "[serviceB] Product.lineItem -> uses the @provides directive but \`Product.lineItem\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.", - }, - ] - `); - }); - - it('warns when there is a @provides on a type that is not a list of entity', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - id: ID! - } - - type LineItem { - sku: String! - quantity: Int! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - lineItems: [LineItem] @provides(fields: "quantity") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateProvidesNotOnEntity({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_NOT_ON_ENTITY", - "message": "[serviceB] Product.lineItems -> uses the @provides directive but \`Product.lineItems\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.", - }, - ] - `); - }); - - it('warns when there is a @provides on a non-object type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - id: ID! - } - - enum Category { - BOOK - MOVIE - SONG - ALBUM - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - category: Category @provides(fields: "id") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateProvidesNotOnEntity({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_NOT_ON_ENTITY", - "message": "[serviceB] Product.category -> uses the @provides directive but \`Product.category\` returns \`Category\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.", - }, - ] - `); - }); - - it('warns when there is a @provides on a list of non-object type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - id: ID! - } - - enum Category { - BOOK - MOVIE - SONG - ALBUM - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - categories: [Category] @provides(fields: "id") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateProvidesNotOnEntity({ schema, serviceList }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "PROVIDES_NOT_ON_ENTITY", - "message": "[serviceB] Product.categories -> uses the @provides directive but \`Product.categories\` returns \`[Category]\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts deleted file mode 100644 index c1e824f8162..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { requiresFieldsMissingExternal as validateRequiresFieldsMissingExternal } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('requiresFieldsMissingExternal', () => { - it('does not warn with proper @requires usage', () => { - const serviceA = { - typeDefs: gql` - type Product { - sku: String! - upc: String! - id: ID! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - id: ID! @external - price: Int! @requires(fields: "id") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateRequiresFieldsMissingExternal({ - schema, - serviceList, - }); - expect(warnings).toEqual([]); - }); - - it('warns when there is a @requires with no matching @external field', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! - id: ID! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Product { - price: Int! @requires(fields: "id") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateRequiresFieldsMissingExternal({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "REQUIRES_FIELDS_MISSING_EXTERNAL", - "message": "[serviceB] Product.price -> requires the field \`id\` to be marked as @external.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts deleted file mode 100644 index 945a5058079..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import gql from 'graphql-tag'; -import { composeServices } from '../../../compose'; -import { requiresFieldsMissingOnBase as validateRequiresFieldsMissingOnBase } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('requiresFieldsMissingOnBase', () => { - it('does not warn with proper @requires usage', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - } - `, - name: 'serviceA', - }; - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - id: ID! - weight: Float! @requires(fields: "sku") - } - `, - name: 'serviceB', - }; - - const serviceList = [serviceA, serviceB]; - const { schema } = composeServices(serviceList); - const warnings = validateRequiresFieldsMissingOnBase({ - schema, - serviceList, - }); - expect(warnings).toEqual([]); - }); - - it('warns when requires selects a field not found on the base type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - } - `, - name: 'serviceA', - }; - const serviceB = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - id: ID! - } - `, - name: 'serviceB', - }; - const serviceC = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - id: ID! @external - weight: Float! @requires(fields: "id") - } - `, - name: 'serviceC', - }; - const serviceList = [serviceA, serviceB, serviceC]; - const { schema } = composeServices(serviceList); - const warnings = validateRequiresFieldsMissingOnBase({ - schema, - serviceList, - }); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "REQUIRES_FIELDS_MISSING_ON_BASE", - "message": "[serviceC] Product.weight -> requires the field \`id\` to be @external. @external fields must exist on the base type, not an extension.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/tsconfig.json b/packages/apollo-federation/src/composition/validate/postComposition/__tests__/tsconfig.json deleted file mode 100644 index a6f70de0bfa..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/__tests__/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../../../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [{ "path": "../../../../../" }] -} diff --git a/packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesIdentical.ts b/packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesIdentical.ts deleted file mode 100644 index a0fa2cb9fa1..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesIdentical.ts +++ /dev/null @@ -1,60 +0,0 @@ -import 'apollo-server-env'; -import { GraphQLError, isSpecifiedDirective, print } from 'graphql'; -import { - errorWithCode, - isFederationDirective, - logDirective, - typeNodesAreEquivalent, - getFederationMetadata, -} from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * A custom directive must be defined identically across all services. This means - * they must have the same name and same locations. Locations are the "on" part of - * a directive, for example: - * directive @stream on FIELD | QUERY - */ -export const executableDirectivesIdentical: PostCompositionValidator = ({ - schema, -}) => { - const errors: GraphQLError[] = []; - - const customDirectives = schema - .getDirectives() - .filter(x => !isFederationDirective(x) && !isSpecifiedDirective(x)); - - customDirectives.forEach(directive => { - const directiveFederationMetadata = getFederationMetadata(directive); - - if (!directiveFederationMetadata) return; - - const definitions = Object.entries( - directiveFederationMetadata.directiveDefinitions, - ); - - // Side-by-side compare all definitions of a single directive, if there's a - // discrepancy in any of those diffs, we should provide an error. - const shouldError = definitions.some(([, definition], index) => { - // Skip the non-comparison step - if (index === 0) return; - const [, previousDefinition] = definitions[index - 1]; - return !typeNodesAreEquivalent(definition, previousDefinition); - }); - - if (shouldError) { - errors.push( - errorWithCode( - 'EXECUTABLE_DIRECTIVES_IDENTICAL', - logDirective(directive.name) + - `custom directives must be defined identically across all services. See below for a list of current implementations:\n${definitions - .map(([serviceName, definition]) => { - return `\t${serviceName}: ${print(definition)}`; - }) - .join('\n')}`, - ), - ); - } - }); - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesInAllServices.ts b/packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesInAllServices.ts deleted file mode 100644 index 131d0bdef35..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/executableDirectivesInAllServices.ts +++ /dev/null @@ -1,60 +0,0 @@ -import 'apollo-server-env'; -import { GraphQLError, isSpecifiedDirective } from 'graphql'; -import { - errorWithCode, - isFederationDirective, - logDirective, - getFederationMetadata, -} from '../../utils'; -import { PostCompositionValidator } from '.'; -/** - * All custom directives with executable locations must be implemented in every - * service. This validator is not responsible for ensuring the directives are an - * ExecutableDirective, however composition ensures this by filtering out all - * TypeSystemDirectiveLocations. - */ -export const executableDirectivesInAllServices: PostCompositionValidator = ({ - schema, - serviceList, -}) => { - const errors: GraphQLError[] = []; - - const customExecutableDirectives = schema - .getDirectives() - .filter(x => !isFederationDirective(x) && !isSpecifiedDirective(x)); - - customExecutableDirectives.forEach(directive => { - const directiveFederationMetadata = getFederationMetadata(directive); - - if (!directiveFederationMetadata) return; - - const allServiceNames = serviceList.map(({ name }) => name); - const serviceNamesWithDirective = Object.keys( - directiveFederationMetadata.directiveDefinitions, - ); - - const serviceNamesWithoutDirective = allServiceNames.reduce( - (without, serviceName) => { - if (!serviceNamesWithDirective.includes(serviceName)) { - without.push(serviceName); - } - return without; - }, - [] as string[], - ); - - if (serviceNamesWithoutDirective.length > 0) { - errors.push( - errorWithCode( - 'EXECUTABLE_DIRECTIVES_IN_ALL_SERVICES', - logDirective(directive.name) + - `Custom directives must be implemented in every service. The following services do not implement the @${ - directive.name - } directive: ${serviceNamesWithoutDirective.join(', ')}.`, - ), - ); - } - }); - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/externalMissingOnBase.ts b/packages/apollo-federation/src/composition/validate/postComposition/externalMissingOnBase.ts deleted file mode 100644 index 1dcaa7bb8f9..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/externalMissingOnBase.ts +++ /dev/null @@ -1,62 +0,0 @@ -import 'apollo-server-env'; -import { isObjectType, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * All fields marked with @external must exist on the base type - */ -export const externalMissingOnBase: PostCompositionValidator = ({ schema }) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(namedType)) continue; - - const typeFederationMetadata = getFederationMetadata(namedType); - - // If externals is populated, we need to look at each one and confirm - // that field exists on base service - if (typeFederationMetadata?.externals) { - // loop over every service that has extensions with @external - for (const [serviceName, externalFieldsForService] of Object.entries( - typeFederationMetadata.externals, - )) { - // for a single service, loop over the external fields. - for (const { field: externalField } of externalFieldsForService) { - const externalFieldName = externalField.name.value; - const allFields = namedType.getFields(); - const matchingBaseField = allFields[externalFieldName]; - - // @external field referenced a field that isn't defined anywhere - if (!matchingBaseField) { - errors.push( - errorWithCode( - 'EXTERNAL_MISSING_ON_BASE', - logServiceAndType(serviceName, typeName, externalFieldName) + - `marked @external but ${externalFieldName} is not defined on the base service of ${typeName} (${typeFederationMetadata.serviceName})`, - ), - ); - continue; - } - - // if the field has a serviceName, then it wasn't defined by the - // service that owns the type - const fieldFederationMetadata = getFederationMetadata(matchingBaseField); - - if (fieldFederationMetadata?.serviceName) { - errors.push( - errorWithCode( - 'EXTERNAL_MISSING_ON_BASE', - logServiceAndType(serviceName, typeName, externalFieldName) + - `marked @external but ${externalFieldName} was defined in ${fieldFederationMetadata.serviceName}, not in the service that owns ${typeName} (${typeFederationMetadata.serviceName})`, - ), - ); - } - } - } - } - } - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/externalTypeMismatch.ts b/packages/apollo-federation/src/composition/validate/postComposition/externalTypeMismatch.ts deleted file mode 100644 index b076bdc1783..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/externalTypeMismatch.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { isObjectType, typeFromAST, isEqualType, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * All fields marked with @external must match the type definition of the base service. - * Additional warning if the type of the @external field doesn't exist at all on the schema - */ -export const externalTypeMismatch: PostCompositionValidator = ({ schema }) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(namedType)) continue; - - // If externals is populated, we need to look at each one and confirm - // there is a matching @requires - const typeFederationMetadata = getFederationMetadata(namedType); - if (typeFederationMetadata?.externals) { - // loop over every service that has extensions with @external - for (const [serviceName, externalFieldsForService] of Object.entries( - typeFederationMetadata.externals, - )) { - // for a single service, loop over the external fields. - for (const { field: externalField } of externalFieldsForService) { - const externalFieldName = externalField.name.value; - const allFields = namedType.getFields(); - const matchingBaseField = allFields[externalFieldName]; - - // FIXME: TypeScript doesn’t currently support passing in a type union - // to an overloaded function like `typeFromAST` - // See https://github.com/Microsoft/TypeScript/issues/14107 - const externalFieldType = typeFromAST( - schema, - externalField.type as any, - ); - - if (!externalFieldType) { - errors.push( - errorWithCode( - 'EXTERNAL_TYPE_MISMATCH', - logServiceAndType(serviceName, typeName, externalFieldName) + - `the type of the @external field does not exist in the resulting composed schema`, - ), - ); - } else if ( - matchingBaseField && - !isEqualType(matchingBaseField.type, externalFieldType) - ) { - errors.push( - errorWithCode( - 'EXTERNAL_TYPE_MISMATCH', - logServiceAndType(serviceName, typeName, externalFieldName) + - `Type \`${externalFieldType.name}\` does not match the type of the original field in ${typeFederationMetadata.serviceName} (\`${matchingBaseField.type}\`)`, - ), - ); - } - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/externalUnused.ts b/packages/apollo-federation/src/composition/validate/postComposition/externalUnused.ts deleted file mode 100644 index 2c671ef211a..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/externalUnused.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { isObjectType, GraphQLError, Kind } from 'graphql'; -import { - findDirectivesOnTypeOrField, - logServiceAndType, - hasMatchingFieldInDirectives, - errorWithCode, - findFieldsThatReturnType, - parseSelections, - isStringValueNode, - selectionIncludesField, - getFederationMetadata, -} from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * for every @external field, there should be a @requires, @key, or @provides - * directive that uses it - */ -export const externalUnused: PostCompositionValidator = ({ schema }) => { - const errors: GraphQLError[] = []; - const types = schema.getTypeMap(); - for (const [parentTypeName, parentType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(parentType)) continue; - // If externals is populated, we need to look at each one and confirm - // it is used - const typeFederationMetadata = getFederationMetadata(parentType); - - // Escape a validation case that's falling through incorrectly. This case - // is handled by `keysMatchBaseService`. - if (typeFederationMetadata) { - const {serviceName, keys} = typeFederationMetadata; - if (serviceName && keys && !keys[serviceName]) continue; - } - - if (typeFederationMetadata?.externals) { - // loop over every service that has extensions with @external - for (const [serviceName, externalFieldsForService] of Object.entries( - typeFederationMetadata.externals, - )) { - // for a single service, loop over the external fields. - for (const { field: externalField } of externalFieldsForService) { - const externalFieldName = externalField.name.value; - - // check the selected fields of every @key provided by `serviceName` - const hasMatchingKeyOnType = Boolean( - hasMatchingFieldInDirectives({ - directives: findDirectivesOnTypeOrField( - parentType.astNode, - 'key', - ), - fieldNameToMatch: externalFieldName, - namedType: parentType, - }), - ); - if (hasMatchingKeyOnType) continue; - - /* - @provides is most commonly used from another type than where - the @external directive is applied. We need to find all - fields on any type in the schema that return this type - and see if they have a provides directive that uses this - external field - - extend type Review { - author: User @provides(fields: "username") - } - - extend type User @key(fields: "id") { - id: ID! @external - username: String @external - reviews: [Review] - } - */ - const hasMatchingProvidesOnAnotherType = findFieldsThatReturnType({ - schema, - typeToFind: parentType, - }).some(field => - findDirectivesOnTypeOrField(field.astNode, 'provides').some( - directive => { - if (!directive.arguments) return false; - const selections = - isStringValueNode(directive.arguments[0].value) && - parseSelections(directive.arguments[0].value.value); - // find the selections which are fields with names matching - // our external field name - return ( - selections && - selections.some( - selection => - selection.kind === Kind.FIELD && - selection.name.value === externalFieldName, - ) - ); - }, - ), - ); - - if (hasMatchingProvidesOnAnotherType) continue; - - /** - * @external fields can be selected by subfields of a selection on another type - * - * For example, with these defs, `canWrite` is marked as external and is - * referenced by a selection set inside the @requires of User.isAdmin - * - * extend type User @key(fields: "id") { - * roles: AccountRoles! - * isAdmin: Boolean! @requires(fields: "roles { canWrite permission { status } }") - * } - * extend type AccountRoles { - * canWrite: Boolean @external - * permission: Permission @external - * } - * - * extend type Permission { - * status: String @external - * } - * - * So, we need to search for fields with requires, then parse the selection sets, - * and try to recursively find the external field's PARENT type, then the external field's name - */ - const hasMatchingRequiresOnAnotherType = Object.values( - schema.getTypeMap(), - ).some(namedType => { - if (!isObjectType(namedType)) return false; - // for every object type, loop over its fields and find fields - // with requires directives - return Object.values(namedType.getFields()).some(field => - findDirectivesOnTypeOrField(field.astNode, 'requires').some( - directive => { - if (!directive.arguments) return false; - const selections = - isStringValueNode(directive.arguments[0].value) && - parseSelections(directive.arguments[0].value.value); - - if (!selections) return false; - return selectionIncludesField({ - selections, - selectionSetType: namedType, - typeToFind: parentType, - fieldToFind: externalFieldName, - }); - }, - ), - ); - }); - - if (hasMatchingRequiresOnAnotherType) continue; - - const hasMatchingRequiresOnType = Object.values( - parentType.getFields(), - ).some(maybeRequiresField => { - const fieldOwner = getFederationMetadata(maybeRequiresField)?.serviceName; - if (fieldOwner !== serviceName) return false; - - const requiresDirectives = findDirectivesOnTypeOrField( - maybeRequiresField.astNode, - 'requires', - ); - - return hasMatchingFieldInDirectives({ - directives: requiresDirectives, - fieldNameToMatch: externalFieldName, - namedType: parentType, - }); - }); - - if (hasMatchingRequiresOnType) continue; - - /** - * @external fields can be required when an interface is returned by - * a field and its concrete implementations need to be defined in a - * service which use non-key fields from other services. Take for example: - * - * // Service A - * type Car implements Vehicle @key(fields: "id") { - * id: ID! - * speed: Int - * } - * - * interface Vehicle { - * id: ID! - * speed: Int - * } - * - * // Service B - * type Query { - * vehicles: [Vehicle] - * } - * - * extend type Car implements Vehicle @key(fields: "id") { - * id: ID! @external - * speed: Int @external - * } - * - * interface Vehicle { - * id: ID! - * speed: Int - * } - * - * Service B defines Car.speed as an external field which is okay - * because it is required for Query.vehicles to exist in the schema - */ - const fieldsOnInterfacesImplementedByParentType: Set = new Set(); - - // Loop over the parent's interfaces - for (const _interface of parentType.getInterfaces()) { - // Collect the field names from each interface in a set - for (const fieldName in _interface.getFields()) { - fieldsOnInterfacesImplementedByParentType.add(fieldName); - } - } - - // If the set contains our field's name, no error is generated - if (fieldsOnInterfacesImplementedByParentType.has(externalFieldName)) { - continue; - } - - errors.push( - errorWithCode( - 'EXTERNAL_UNUSED', - logServiceAndType( - serviceName, - parentTypeName, - externalFieldName, - ) + - `is marked as @external but is not used by a @requires, @key, or @provides directive.`, - ), - ); - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/index.ts b/packages/apollo-federation/src/composition/validate/postComposition/index.ts deleted file mode 100644 index 6e7a35189db..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GraphQLSchema, GraphQLError } from 'graphql'; -import { ServiceDefinition } from '../../types'; - -export { externalUnused } from './externalUnused'; -export { externalMissingOnBase } from './externalMissingOnBase'; -export { externalTypeMismatch } from './externalTypeMismatch'; -export { requiresFieldsMissingExternal } from './requiresFieldsMissingExternal'; -export { requiresFieldsMissingOnBase } from './requiresFieldsMissingOnBase'; -export { keyFieldsMissingOnBase } from './keyFieldsMissingOnBase'; -export { keyFieldsSelectInvalidType } from './keyFieldsSelectInvalidType'; -export { providesFieldsMissingExternal } from './providesFieldsMissingExternal'; -export { - providesFieldsSelectInvalidType, -} from './providesFieldsSelectInvalidType'; -export { providesNotOnEntity } from './providesNotOnEntity'; -export { - executableDirectivesInAllServices, -} from './executableDirectivesInAllServices'; -export { executableDirectivesIdentical } from './executableDirectivesIdentical'; -export { keysMatchBaseService } from './keysMatchBaseService'; - -export type PostCompositionValidator = ({ - schema, - serviceList, -}: { - schema: GraphQLSchema; - serviceList: ServiceDefinition[]; -}) => GraphQLError[]; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/packages/apollo-federation/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts deleted file mode 100644 index a2d7384a6a8..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * - The fields argument can not select fields that were overwritten by another service - */ -export const keyFieldsMissingOnBase: PostCompositionValidator = ({ - schema, -}) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - if (!isObjectType(namedType)) continue; - - const typeFederationMetadata = getFederationMetadata(namedType); - if (typeFederationMetadata?.keys) { - const allFieldsInType = namedType.getFields(); - for (const [serviceName, selectionSets] of Object.entries( - typeFederationMetadata.keys, - )) { - for (const selectionSet of selectionSets) { - for (const field of selectionSet as FieldNode[]) { - const name = field.name.value; - - // find corresponding field for each selected field - const matchingField = allFieldsInType[name]; - - // NOTE: We don't need to warn if there is no matching field. - // keyFieldsSelectInvalidType already does that :) - if (matchingField) { - const fieldFederationMetadata = getFederationMetadata(matchingField); - // warn if not from base type OR IF IT WAS OVERWITTEN - if (fieldFederationMetadata?.serviceName) { - errors.push( - errorWithCode( - 'KEY_FIELDS_MISSING_ON_BASE', - logServiceAndType(serviceName, typeName) + - `A @key selects ${name}, but ${typeName}.${name} was either created or overwritten by ${fieldFederationMetadata.serviceName}, not ${serviceName}`, - ), - ); - } - } - } - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/packages/apollo-federation/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts deleted file mode 100644 index 63d089ca90f..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - isObjectType, - FieldNode, - isInterfaceType, - isNonNullType, - getNullableType, - isUnionType, - GraphQLError, -} from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * - The fields argument can not have root fields that result in a list - * - The fields argument can not have root fields that result in an interface - * - The fields argument can not have root fields that result in a union type - */ -export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ - schema, -}) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - if (!isObjectType(namedType)) continue; - - const typeFederationMetadata = getFederationMetadata(namedType); - if (typeFederationMetadata?.keys) { - const allFieldsInType = namedType.getFields(); - for (const [serviceName, selectionSets] of Object.entries( - typeFederationMetadata.keys, - )) { - for (const selectionSet of selectionSets) { - for (const field of selectionSet as FieldNode[]) { - const name = field.name.value; - - // find corresponding field for each selected field - const matchingField = allFieldsInType[name]; - if (!matchingField) { - errors.push( - errorWithCode( - 'KEY_FIELDS_SELECT_INVALID_TYPE', - logServiceAndType(serviceName, typeName) + - `A @key selects ${name}, but ${typeName}.${name} could not be found`, - ), - ); - } - - if (matchingField) { - if ( - isInterfaceType(matchingField.type) || - (isNonNullType(matchingField.type) && - isInterfaceType(getNullableType(matchingField.type))) - ) { - errors.push( - errorWithCode( - 'KEY_FIELDS_SELECT_INVALID_TYPE', - logServiceAndType(serviceName, typeName) + - `A @key selects ${typeName}.${name}, which is an interface type. Keys cannot select interfaces.`, - ), - ); - } - - if ( - isUnionType(matchingField.type) || - (isNonNullType(matchingField.type) && - isUnionType(getNullableType(matchingField.type))) - ) { - errors.push( - errorWithCode( - 'KEY_FIELDS_SELECT_INVALID_TYPE', - logServiceAndType(serviceName, typeName) + - `A @key selects ${typeName}.${name}, which is a union type. Keys cannot select union types.`, - ), - ); - } - } - } - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/keysMatchBaseService.ts b/packages/apollo-federation/src/composition/validate/postComposition/keysMatchBaseService.ts deleted file mode 100644 index 62e4cc57592..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/keysMatchBaseService.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { isObjectType, GraphQLError, SelectionNode } from 'graphql'; -import { - logServiceAndType, - errorWithCode, - getFederationMetadata, -} from '../../utils'; -import { PostCompositionValidator } from '.'; -import { printWithReducedWhitespace } from '../../../service'; - -/** - * 1. KEY_MISSING_ON_BASE - Originating types must specify at least 1 @key directive - * 2. MULTIPLE_KEYS_ON_EXTENSION - Extending services may not use more than 1 @key directive - * 3. KEY_NOT_SPECIFIED - Extending services must use a valid @key specified by the originating type - */ -export const keysMatchBaseService: PostCompositionValidator = function ({ - schema, -}) { - const errors: GraphQLError[] = []; - const types = schema.getTypeMap(); - for (const [parentTypeName, parentType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(parentType)) continue; - - const typeFederationMetadata = getFederationMetadata(parentType); - - if (typeFederationMetadata) { - const { serviceName, keys } = typeFederationMetadata; - - if (serviceName && keys) { - if (!keys[serviceName]) { - errors.push( - errorWithCode( - 'KEY_MISSING_ON_BASE', - logServiceAndType(serviceName, parentTypeName) + - `appears to be an entity but no @key directives are specified on the originating type.`, - ), - ); - continue; - } - - const availableKeys = keys[serviceName].map(printFieldSet); - Object.entries(keys) - // No need to validate that the owning service matches its specified keys - .filter(([service]) => service !== serviceName) - .forEach(([extendingService, keyFields]) => { - // Extensions can't specify more than one key - if (keyFields.length > 1) { - errors.push( - errorWithCode( - 'MULTIPLE_KEYS_ON_EXTENSION', - logServiceAndType(extendingService, parentTypeName) + - `is extended from service ${serviceName} but specifies multiple @key directives. Extensions may only specify one @key.`, - ), - ); - return; - } - - // This isn't representative of an invalid graph, but it is an existing - // limitation of the query planner that we want to validate against for now. - // In the future, `@key`s just need to be "reachable" through a number of - // services which can link one key to another via "joins". - const extensionKey = printFieldSet(keyFields[0]); - if (!availableKeys.includes(extensionKey)) { - errors.push( - errorWithCode( - 'KEY_NOT_SPECIFIED', - logServiceAndType(extendingService, parentTypeName) + - `extends from ${serviceName} but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are:\n` + - `\t${availableKeys - .map((fieldSet) => `@key(fields: "${fieldSet}")`) - .join('\n\t')}`, - ), - ); - return; - } - }); - } - } - } - - return errors; -}; - -function printFieldSet(selections: readonly SelectionNode[]): string { - return selections.map(printWithReducedWhitespace).join(' '); -} diff --git a/packages/apollo-federation/src/composition/validate/postComposition/providesFieldsMissingExternal.ts b/packages/apollo-federation/src/composition/validate/postComposition/providesFieldsMissingExternal.ts deleted file mode 100644 index 6a4cb2eef57..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/providesFieldsMissingExternal.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * for every field in a @provides, there should be a matching @external - */ -export const providesFieldsMissingExternal: PostCompositionValidator = ({ - schema, -}) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(namedType)) continue; - - // for each field, if there's a requires on it, check that there's a matching - // @external field, and that the types referenced are from the base type - for (const [fieldName, field] of Object.entries(namedType.getFields())) { - const fieldFederationMetadata = getFederationMetadata(field); - const serviceName = fieldFederationMetadata?.serviceName; - - // serviceName should always exist on fields that have @provides federation data, since - // the only case where serviceName wouldn't exist is on a base type, and in that case, - // the `provides` metadata should never get added to begin with. This should be caught in - // composition work. This kind of error should be validated _before_ composition. - if (!serviceName) continue; - - const fieldType = field.type; - if (!isObjectType(fieldType)) continue; - - const fieldTypeFederationMetadata = getFederationMetadata(fieldType); - - const externalFieldsOnTypeForService = fieldTypeFederationMetadata?.externals?.[serviceName]; - - if (fieldFederationMetadata?.provides) { - const selections = fieldFederationMetadata.provides as FieldNode[]; - for (const selection of selections) { - const foundMatchingExternal = externalFieldsOnTypeForService - ? externalFieldsOnTypeForService.some( - ext => ext.field.name.value === selection.name.value, - ) - : undefined; - if (!foundMatchingExternal) { - errors.push( - errorWithCode( - 'PROVIDES_FIELDS_MISSING_EXTERNAL', - logServiceAndType(serviceName, typeName, fieldName) + - `provides the field \`${selection.name.value}\` and requires ${fieldType}.${selection.name.value} to be marked as @external.`, - ), - ); - } - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts b/packages/apollo-federation/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts deleted file mode 100644 index 8b308475eec..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - GraphQLError, - isObjectType, - FieldNode, - isListType, - isInterfaceType, - isNonNullType, - getNullableType, - isUnionType, -} from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * - The fields argument can not have root fields that result in a list - * - The fields argument can not have root fields that result in an interface - * - The fields argument can not have root fields that result in a union type - */ -export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ - schema, -}) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - if (!isObjectType(namedType)) continue; - - // for each field, if there's a provides on it, check the type of the field - // it references - for (const [fieldName, field] of Object.entries(namedType.getFields())) { - const fieldFederationMetadata = getFederationMetadata(field); - const serviceName = fieldFederationMetadata?.serviceName; - - // serviceName should always exist on fields that have @provides federation data, since - // the only case where serviceName wouldn't exist is on a base type, and in that case, - // the `provides` metadata should never get added to begin with. This should be caught in - // composition work. This kind of error should be validated _before_ composition. - if (!serviceName) continue; - - const fieldType = field.type; - if (!isObjectType(fieldType)) continue; - const allFields = fieldType.getFields(); - - if (fieldFederationMetadata?.provides) { - const selections = fieldFederationMetadata.provides as FieldNode[]; - for (const selection of selections) { - const name = selection.name.value; - const matchingField = allFields[name]; - if (!matchingField) { - errors.push( - errorWithCode( - 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', - logServiceAndType(serviceName, typeName, fieldName) + - `A @provides selects ${name}, but ${fieldType.name}.${name} could not be found`, - ), - ); - continue; - } - - if ( - isListType(matchingField.type) || - (isNonNullType(matchingField.type) && - isListType(getNullableType(matchingField.type))) - ) { - errors.push( - errorWithCode( - 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', - logServiceAndType(serviceName, typeName, fieldName) + - `A @provides selects ${fieldType.name}.${name}, which is a list type. A field cannot @provide lists.`, - ), - ); - } - if ( - isInterfaceType(matchingField.type) || - (isNonNullType(matchingField.type) && - isInterfaceType(getNullableType(matchingField.type))) - ) { - errors.push( - errorWithCode( - 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', - logServiceAndType(serviceName, typeName, fieldName) + - `A @provides selects ${fieldType.name}.${name}, which is an interface type. A field cannot @provide interfaces.`, - ), - ); - } - - if ( - isUnionType(matchingField.type) || - (isNonNullType(matchingField.type) && - isUnionType(getNullableType(matchingField.type))) - ) { - errors.push( - errorWithCode( - 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', - logServiceAndType(serviceName, typeName, fieldName) + - `A @provides selects ${fieldType.name}.${name}, which is a union type. A field cannot @provide union types.`, - ), - ); - } - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/providesNotOnEntity.ts b/packages/apollo-federation/src/composition/validate/postComposition/providesNotOnEntity.ts deleted file mode 100644 index 6fd8f362ad6..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/providesNotOnEntity.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - isObjectType, - GraphQLError, - isListType, - isNonNullType, -} from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * Provides directive can only be added to return types that are entities - */ -export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(namedType)) continue; - - // for each field, if there's a provides on it, check that the containing - // type has a `key` field under the federation metadata. - for (const [fieldName, field] of Object.entries(namedType.getFields())) { - const fieldFederationMetadata = getFederationMetadata(field) - const serviceName = fieldFederationMetadata?.serviceName; - - // serviceName should always exist on fields that have @provides federation data, since - // the only case where serviceName wouldn't exist is on a base type, and in that case, - // the `provides` metadata should never get added to begin with. This should be caught in - // composition work. This kind of error should be validated _before_ composition. - if ( - !serviceName && - fieldFederationMetadata?.provides && - !fieldFederationMetadata?.belongsToValueType - ) - throw Error( - 'Internal Consistency Error: field with provides information does not have service name.', - ); - if (!serviceName) continue; - - const getBaseType = (type: any): any => - isListType(type) || isNonNullType(type) - ? getBaseType(type.ofType) - : type; - const baseType = getBaseType(field.type); - - // field has a @provides directive on it - if (fieldFederationMetadata?.provides) { - if (!isObjectType(baseType)) { - errors.push( - errorWithCode( - 'PROVIDES_NOT_ON_ENTITY', - logServiceAndType(serviceName, typeName, fieldName) + - `uses the @provides directive but \`${typeName}.${fieldName}\` returns \`${field.type}\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.`, - ), - ); - continue; - } - - const fieldType = types[baseType.name]; - const selectedFieldIsEntity = getFederationMetadata(fieldType)?.keys; - - if (!selectedFieldIsEntity) { - errors.push( - errorWithCode( - 'PROVIDES_NOT_ON_ENTITY', - logServiceAndType(serviceName, typeName, fieldName) + - `uses the @provides directive but \`${typeName}.${fieldName}\` does not return a type that has a @key. Try adding a @key to the \`${baseType}\` type.`, - ), - ); - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts deleted file mode 100644 index 02c049f6971..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * for every @requires, there should be a matching @external - */ -export const requiresFieldsMissingExternal: PostCompositionValidator = ({ - schema, -}) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(namedType)) continue; - - // for each field, if there's a requires on it, check that there's a matching - // @external field, and that the types referenced are from the base type - for (const [fieldName, field] of Object.entries(namedType.getFields())) { - const fieldFederationMetadata = getFederationMetadata(field); - const serviceName = fieldFederationMetadata?.serviceName; - - // serviceName should always exist on fields that have @requires federation data, since - // the only case where serviceName wouldn't exist is on a base type, and in that case, - // the `requires` metadata should never get added to begin with. This should be caught in - // composition work. This kind of error should be validated _before_ composition. - if (!serviceName) continue; - - if (fieldFederationMetadata?.requires) { - const typeFederationMetadata = getFederationMetadata(namedType); - const externalFieldsOnTypeForService = - typeFederationMetadata?.externals?.[serviceName]; - - const selections = fieldFederationMetadata?.requires as FieldNode[]; - for (const selection of selections) { - const foundMatchingExternal = externalFieldsOnTypeForService - ? externalFieldsOnTypeForService.some( - ext => ext.field.name.value === selection.name.value, - ) - : undefined; - if (!foundMatchingExternal) { - errors.push( - errorWithCode( - 'REQUIRES_FIELDS_MISSING_EXTERNAL', - logServiceAndType(serviceName, typeName, fieldName) + - `requires the field \`${selection.name.value}\` to be marked as @external.`, - ), - ); - } - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts deleted file mode 100644 index 401dcd6678d..00000000000 --- a/packages/apollo-federation/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; -import { PostCompositionValidator } from '.'; - -/** - * The fields arg in @requires can only reference fields on the base type - */ -export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ - schema, -}) => { - const errors: GraphQLError[] = []; - - const types = schema.getTypeMap(); - for (const [typeName, namedType] of Object.entries(types)) { - // Only object types have fields - if (!isObjectType(namedType)) continue; - - // for each field, if there's a requires on it, check that there's a matching - // @external field, and that the types referenced are from the base type - for (const [fieldName, field] of Object.entries(namedType.getFields())) { - const fieldFederationMetadata = getFederationMetadata(field); - const serviceName = fieldFederationMetadata?.serviceName; - - // serviceName should always exist on fields that have @requires federation data, since - // the only case where serviceName wouldn't exist is on a base type, and in that case, - // the `requires` metadata should never get added to begin with. This should be caught in - // composition work. This kind of error should be validated _before_ composition. - if (!serviceName) continue; - - if (fieldFederationMetadata?.requires) { - const selections = fieldFederationMetadata.requires as FieldNode[]; - for (const selection of selections) { - // check the selections are from the _base_ type (no serviceName) - const matchingFieldOnType = namedType.getFields()[ - selection.name.value - ]; - const typeFederationMetadata = getFederationMetadata(matchingFieldOnType); - - if (typeFederationMetadata?.serviceName) { - errors.push( - errorWithCode( - 'REQUIRES_FIELDS_MISSING_ON_BASE', - logServiceAndType(serviceName, typeName, fieldName) + - `requires the field \`${selection.name.value}\` to be @external. @external fields must exist on the base type, not an extension.`, - ), - ); - } - } - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts b/packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts deleted file mode 100644 index cde9552c3bf..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import gql from 'graphql-tag'; -import { duplicateEnumOrScalar as validateDuplicateEnumOrScalar } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('duplicateEnumOrScalar', () => { - it('does not error with proper enum and scalar usage', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - shippingDate: Date - type: ProductType - } - - enum ProductType { - BOOK - FURNITURE - } - - extend enum ProductType { - DIGITAL - } - - scalar Date - `, - name: 'serviceA', - }; - - const warnings = validateDuplicateEnumOrScalar(serviceA); - expect(warnings).toEqual([]); - }); - it('errors when there are multiple definitions of the same enum', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - - enum ProductType { - BOOK - FURNITURE - } - - enum ProductType { - DIGITAL - } - `, - name: 'serviceA', - }; - - const warnings = validateDuplicateEnumOrScalar(serviceA); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "DUPLICATE_ENUM_DEFINITION", - "message": "[serviceA] ProductType -> The enum, \`ProductType\` was defined multiple times in this service. Remove one of the definitions for \`ProductType\`", - }, - ] - `); - }); - - it('errors when there are multiple definitions of the same scalar', () => { - const serviceA = { - typeDefs: gql` - scalar Date - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - deliveryDate: Date - } - - scalar Date - `, - name: 'serviceA', - }; - - const warnings = validateDuplicateEnumOrScalar(serviceA); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "DUPLICATE_SCALAR_DEFINITION", - "message": "[serviceA] Date -> The scalar, \`Date\` was defined multiple times in this service. Remove one of the definitions for \`Date\`", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts b/packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts deleted file mode 100644 index 6bc91a5662e..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import gql from 'graphql-tag'; -import { duplicateEnumValue as validateDuplicateEnumValue } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('duplicateEnumValue', () => { - it('does not error with proper enum usage', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - - enum ProductType { - BOOK - FURNITURE - } - - extend enum ProductType { - DIGITAL - } - `, - name: 'serviceA', - }; - - const warnings = validateDuplicateEnumValue(serviceA); - expect(warnings).toEqual([]); - }); - it('errors when there are duplicate enum values in a single service', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - - enum ProductType { - BOOK - FURNITURE - } - - extend enum ProductType { - DIGITAL - BOOK - } - `, - name: 'serviceA', - }; - - const warnings = validateDuplicateEnumValue(serviceA); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "DUPLICATE_ENUM_VALUE", - "message": "[serviceA] ProductType.BOOK -> The enum, \`ProductType\` has multiple definitions of the \`BOOK\` value.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts b/packages/apollo-federation/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts deleted file mode 100644 index 2b1cad013cb..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import gql from 'graphql-tag'; -import { externalUsedOnBase as validateExternalUsedOnBase } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('externalUsedOnBase', () => { - it('does not warn when no externals directives are defined', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const warnings = validateExternalUsedOnBase(serviceA); - expect(warnings).toEqual([]); - }); - - it('warns when there is a @external field on a base type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! @external - id: ID! - } - `, - name: 'serviceA', - }; - - const warnings = validateExternalUsedOnBase(serviceA); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTERNAL_USED_ON_BASE", - "message": "[serviceA] Product.upc -> Found extraneous @external directive. @external cannot be used on base types.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts b/packages/apollo-federation/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts deleted file mode 100644 index 3811694310e..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import gql from 'graphql-tag'; -import { keyFieldsMissingExternal as validateKeyFieldsMissingExternal } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('keyFieldsMissingExternal', () => { - it('has no warnings when @key fields reference an @external field', () => { - const serviceA = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! @external - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const warnings = validateKeyFieldsMissingExternal(serviceA); - expect(warnings).toHaveLength(0); - }); - - it('has no warnings with correct selection set / nested @external usage', () => { - const serviceA = { - typeDefs: gql` - extend type Car @key(fields: "model { name kit { upc } } year") { - model: Model! @external - year: String! @external - color: String! - } - - extend type Model { - name: String! @external - kit: Kit @external - } - - extend type Kit { - upc: String! @external - } - `, - name: 'serviceA', - }; - - const warnings = validateKeyFieldsMissingExternal(serviceA); - expect(warnings).toHaveLength(0); - }); - - it('has no warnings with @deprecated directive usage', () => { - const serviceA = { - typeDefs: gql` - extend type Car @key(fields: "model { name kit { upc } } year") { - model: Model! @external - year: String! @external - color: String! @deprecated(reason: "Use colors instead") - colors: Color! - } - - extend type Model { - name: String! @external - kit: Kit @external - } - - extend type Kit { - upc: String! @external - } - - enum Color { - Red - Blue - } - `, - name: 'serviceA', - }; - - const warnings = validateKeyFieldsMissingExternal(serviceA); - expect(warnings).toHaveLength(0); - }); - - it("warns when a @key argument doesn't reference an @external field", () => { - const serviceA = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const warnings = validateKeyFieldsMissingExternal(serviceA); - expect(warnings).toHaveLength(1); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "KEY_FIELDS_MISSING_EXTERNAL", - "message": "[serviceA] Product -> A @key directive specifies the \`sku\` field which has no matching @external field.", - }, - ] - `); - }); - - it("warns when a @key argument references a field that isn't known", () => { - const serviceA = { - typeDefs: gql` - extend type Product @key(fields: "sku") { - upc: String! @external - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const warnings = validateKeyFieldsMissingExternal(serviceA); - expect(warnings).toHaveLength(1); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "KEY_FIELDS_MISSING_EXTERNAL", - "message": "[serviceA] Product -> A @key directive specifies a field which is not found in this service. Add a field to this type with @external.", - }, - ] - `); - }); - - it("warns when a @key argument doesn't reference an @external field", () => { - const serviceA = { - typeDefs: gql` - extend type Car @key(fields: "model { name kit { upc } } year") { - model: Model! @external - year: String! @external - } - - extend type Model { - name: String! - kit: Kit - } - - type Kit { - upc: String! - } - `, - name: 'serviceA', - }; - - const warnings = validateKeyFieldsMissingExternal(serviceA); - expect(warnings).toHaveLength(3); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "KEY_FIELDS_MISSING_EXTERNAL", - "message": "[serviceA] Model -> A @key directive specifies the \`name\` field which has no matching @external field.", - }, - Object { - "code": "KEY_FIELDS_MISSING_EXTERNAL", - "message": "[serviceA] Model -> A @key directive specifies the \`kit\` field which has no matching @external field.", - }, - Object { - "code": "KEY_FIELDS_MISSING_EXTERNAL", - "message": "[serviceA] Kit -> A @key directive specifies the \`upc\` field which has no matching @external field.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts b/packages/apollo-federation/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts deleted file mode 100644 index fc5d42c66f2..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import gql from 'graphql-tag'; -import { requiresUsedOnBase as validateRequiresUsedOnBase } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('requiresUsedOnBase', () => { - it('does not warn when no requires directives are defined', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "color { id value }") { - sku: String! - upc: String! - color: Color! - } - - type Color { - id: ID! - value: String! - } - `, - name: 'serviceA', - }; - - const warnings = validateRequiresUsedOnBase(serviceA); - expect(warnings).toEqual([]); - }); - - it('warns when there is a @requires field on a base type', () => { - const serviceA = { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: String! - upc: String! @requires(fields: "sku") - id: ID! - } - `, - name: 'serviceA', - }; - - const warnings = validateRequiresUsedOnBase(serviceA); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "REQUIRES_USED_ON_BASE", - "message": "[serviceA] Product.upc -> Found extraneous @requires directive. @requires cannot be used on base types.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts b/packages/apollo-federation/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts deleted file mode 100644 index 8d03a00e732..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import gql from 'graphql-tag'; -import { reservedFieldUsed as validateReservedFieldUsed } from '..'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('reservedFieldUsed', () => { - it('has no warnings when _service and _entities arent used', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - } - - type Product { - sku: String - } - `, - name: 'serviceA', - }; - - const warnings = validateReservedFieldUsed(serviceA); - expect(warnings).toEqual([]); - }); - - it('warns when _service or _entities is used at the query root', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - _service: String! - _entities: String! - } - - type Product { - sku: String - } - `, - name: 'serviceA', - }; - - const warnings = validateReservedFieldUsed(serviceA); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "RESERVED_FIELD_USED", - "message": "[serviceA] Query._service -> _service is a field reserved for federation and can't be used at the Query root.", - }, - Object { - "code": "RESERVED_FIELD_USED", - "message": "[serviceA] Query._entities -> _entities is a field reserved for federation and can't be used at the Query root.", - }, - ] - `); - }); - - it('warns when _service or _entities is used in a schema extension', () => { - const schemaDefinition = { - typeDefs: gql` - schema { - query: RootQuery - } - - type RootQuery { - product: Product - _entities: String! - } - - type Product { - sku: String - } - `, - name: 'schemaDefinition', - }; - - const schemaExtension = { - typeDefs: gql` - extend schema { - query: RootQuery - } - - type RootQuery { - _service: String - product: Product - } - - type Product { - sku: String - } - `, - name: 'schemaExtension', - }; - - const schemaDefinitionWarnings = validateReservedFieldUsed( - schemaDefinition, - ); - const schemaExtensionWarnings = validateReservedFieldUsed(schemaExtension); - - expect(schemaDefinitionWarnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "RESERVED_FIELD_USED", - "message": "[schemaDefinition] RootQuery._entities -> _entities is a field reserved for federation and can't be used at the Query root.", - }, - ] - `); - expect(schemaExtensionWarnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "RESERVED_FIELD_USED", - "message": "[schemaExtension] RootQuery._service -> _service is a field reserved for federation and can't be used at the Query root.", - }, - ] - `); - }); - - it('warns when reserved fields are used on custom Query types', () => { - const serviceA = { - typeDefs: gql` - schema { - query: RootQuery - } - - type RootQuery { - product: Product - _service: String - _entities: String - } - - type Product { - sku: String - } - `, - name: 'serviceA', - }; - - const warnings = validateReservedFieldUsed(serviceA); - - expect(warnings).toHaveLength(2); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "RESERVED_FIELD_USED", - "message": "[serviceA] RootQuery._service -> _service is a field reserved for federation and can't be used at the Query root.", - }, - Object { - "code": "RESERVED_FIELD_USED", - "message": "[serviceA] RootQuery._entities -> _entities is a field reserved for federation and can't be used at the Query root.", - }, - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/tsconfig.json b/packages/apollo-federation/src/composition/validate/preComposition/__tests__/tsconfig.json deleted file mode 100644 index a6f70de0bfa..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/__tests__/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../../../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [{ "path": "../../../../../" }] -} diff --git a/packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumOrScalar.ts b/packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumOrScalar.ts deleted file mode 100644 index b3752056465..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumOrScalar.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { visit, GraphQLError } from 'graphql'; -import { ServiceDefinition } from '../../types'; - -import { logServiceAndType, errorWithCode } from '../../utils'; - -export const duplicateEnumOrScalar = ({ - name: serviceName, - typeDefs, -}: ServiceDefinition) => { - const errors: GraphQLError[] = []; - - // keep track of every enum and scalar and error if there are ever duplicates - const enums: string[] = []; - const scalars: string[] = []; - - visit(typeDefs, { - EnumTypeDefinition(definition) { - const name = definition.name.value; - if (enums.includes(name)) { - errors.push( - errorWithCode( - 'DUPLICATE_ENUM_DEFINITION', - logServiceAndType(serviceName, name) + - `The enum, \`${name}\` was defined multiple times in this service. Remove one of the definitions for \`${name}\``, - ), - ); - return definition; - } - enums.push(name); - return definition; - }, - ScalarTypeDefinition(definition) { - const name = definition.name.value; - if (scalars.includes(name)) { - errors.push( - errorWithCode( - 'DUPLICATE_SCALAR_DEFINITION', - logServiceAndType(serviceName, name) + - `The scalar, \`${name}\` was defined multiple times in this service. Remove one of the definitions for \`${name}\``, - ), - ); - return definition; - } - scalars.push(name); - return definition; - }, - }); - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumValue.ts b/packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumValue.ts deleted file mode 100644 index 5e83eb417e7..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/duplicateEnumValue.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { visit, GraphQLError } from 'graphql'; -import { ServiceDefinition } from '../../types'; - -import { logServiceAndType, errorWithCode } from '../../utils'; - -export const duplicateEnumValue = ({ - name: serviceName, - typeDefs, -}: ServiceDefinition) => { - const errors: GraphQLError[] = []; - - const enums: { [name: string]: string[] } = {}; - - visit(typeDefs, { - EnumTypeDefinition(definition) { - const name = definition.name.value; - const enumValues = - definition.values && definition.values.map(value => value.name.value); - - if (!enumValues) return definition; - - if (enums[name] && enums[name].length) { - enumValues.map(valueName => { - if (enums[name].includes(valueName)) { - errors.push( - errorWithCode( - 'DUPLICATE_ENUM_VALUE', - logServiceAndType(serviceName, name, valueName) + - `The enum, \`${name}\` has multiple definitions of the \`${valueName}\` value.`, - ), - ); - return; - } - enums[name].push(valueName); - }); - } else { - enums[name] = enumValues; - } - - return definition; - }, - EnumTypeExtension(definition) { - const name = definition.name.value; - const enumValues = - definition.values && definition.values.map(value => value.name.value); - - if (!enumValues) return definition; - - if (enums[name] && enums[name].length) { - enumValues.map(valueName => { - if (enums[name].includes(valueName)) { - errors.push( - errorWithCode( - 'DUPLICATE_ENUM_VALUE', - logServiceAndType(serviceName, name, valueName) + - `The enum, \`${name}\` has multiple definitions of the \`${valueName}\` value.`, - ), - ); - return; - } - enums[name].push(valueName); - }); - } else { - enums[name] = enumValues; - } - - return definition; - }, - }); - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/externalUsedOnBase.ts b/packages/apollo-federation/src/composition/validate/preComposition/externalUsedOnBase.ts deleted file mode 100644 index ddb2b2bd02f..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/externalUsedOnBase.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { visit, GraphQLError } from 'graphql'; -import { ServiceDefinition } from '../../types'; - -import { logServiceAndType, errorWithCode } from '../../utils'; - -/** - * - There are no fields with @external on base type definitions - */ -export const externalUsedOnBase = ({ - name: serviceName, - typeDefs, -}: ServiceDefinition) => { - const errors: GraphQLError[] = []; - - visit(typeDefs, { - ObjectTypeDefinition(typeDefinition) { - if (typeDefinition.fields) { - for (const field of typeDefinition.fields) { - if (field.directives) { - for (const directive of field.directives) { - const name = directive.name.value; - if (name === 'external') { - errors.push( - errorWithCode( - 'EXTERNAL_USED_ON_BASE', - logServiceAndType( - serviceName, - typeDefinition.name.value, - field.name.value, - ) + - `Found extraneous @external directive. @external cannot be used on base types.`, - ), - ); - } - } - } - } - } - }, - }); - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/index.ts b/packages/apollo-federation/src/composition/validate/preComposition/index.ts deleted file mode 100644 index c2d78e45bfe..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { externalUsedOnBase } from './externalUsedOnBase'; -export { requiresUsedOnBase } from './requiresUsedOnBase'; -export { keyFieldsMissingExternal } from './keyFieldsMissingExternal'; -export { reservedFieldUsed } from './reservedFieldUsed'; -export { duplicateEnumOrScalar } from './duplicateEnumOrScalar'; -export { duplicateEnumValue } from './duplicateEnumValue'; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/keyFieldsMissingExternal.ts b/packages/apollo-federation/src/composition/validate/preComposition/keyFieldsMissingExternal.ts deleted file mode 100644 index eb8fc648df0..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/keyFieldsMissingExternal.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - visit, - visitWithTypeInfo, - TypeInfo, - parse, - GraphQLSchema, - GraphQLError, - specifiedDirectives, -} from 'graphql'; -import { buildSchemaFromSDL } from 'apollo-graphql'; -import { federationDirectives } from '../../../directives'; -import { ServiceDefinition } from '../../types'; -import { - findDirectivesOnTypeOrField, - isStringValueNode, - logServiceAndType, - errorWithCode, - isNotNullOrUndefined -} from '../../utils'; - -/** - * For every @key directive, it must reference a field marked as @external - */ -export const keyFieldsMissingExternal = ({ - name: serviceName, - typeDefs, -}: ServiceDefinition) => { - const errors: GraphQLError[] = []; - - // Build an array that accounts for all key directives on type extensions. - let keyDirectiveInfoOnTypeExtensions: { - typeName: string; - keyArgument: string; - }[] = []; - visit(typeDefs, { - ObjectTypeExtension(node) { - const keyDirectivesOnTypeExtension = findDirectivesOnTypeOrField( - node, - 'key', - ); - - const keyDirectivesInfo = keyDirectivesOnTypeExtension - .map(keyDirective => - keyDirective.arguments && - isStringValueNode(keyDirective.arguments[0].value) - ? { - typeName: node.name.value, - keyArgument: keyDirective.arguments[0].value.value, - } - : null, - ) - .filter(isNotNullOrUndefined); - - keyDirectiveInfoOnTypeExtensions.push(...keyDirectivesInfo); - }, - }); - - // this allows us to build a partial schema - let schema = new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }); - try { - schema = buildSchemaFromSDL(typeDefs, schema); - } catch (e) { - errors.push(e); - return errors; - } - - const typeInfo = new TypeInfo(schema); - - for (const { typeName, keyArgument } of keyDirectiveInfoOnTypeExtensions) { - const keyDirectiveSelectionSet = parse( - `fragment __generated on ${typeName} { ${keyArgument} }`, - ); - visit( - keyDirectiveSelectionSet, - visitWithTypeInfo(typeInfo, { - Field() { - const fieldDef = typeInfo.getFieldDef(); - const parentType = typeInfo.getParentType(); - if (parentType) { - if (!fieldDef) { - // TODO: find all fields that have @external and suggest them / heursitic match - errors.push( - errorWithCode( - 'KEY_FIELDS_MISSING_EXTERNAL', - logServiceAndType(serviceName, parentType.name) + - `A @key directive specifies a field which is not found in this service. Add a field to this type with @external.`, - ), - ); - return; - } - const externalDirectivesOnField = findDirectivesOnTypeOrField( - fieldDef.astNode, - 'external', - ); - - if (externalDirectivesOnField.length === 0) { - errors.push( - errorWithCode( - 'KEY_FIELDS_MISSING_EXTERNAL', - logServiceAndType(serviceName, parentType.name) + - `A @key directive specifies the \`${fieldDef.name}\` field which has no matching @external field.`, - ), - ); - } - } - }, - }), - ); - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/requiresUsedOnBase.ts b/packages/apollo-federation/src/composition/validate/preComposition/requiresUsedOnBase.ts deleted file mode 100644 index a70a32b3523..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/requiresUsedOnBase.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { GraphQLError, visit } from 'graphql'; -import { ServiceDefinition } from '../../types'; - -import { logServiceAndType, errorWithCode } from '../../utils'; - -/** - * - There are no fields with @requires on base type definitions - */ -export const requiresUsedOnBase = ({ - name: serviceName, - typeDefs, -}: ServiceDefinition) => { - const errors: GraphQLError[] = []; - - visit(typeDefs, { - ObjectTypeDefinition(typeDefinition) { - if (typeDefinition.fields) { - for (const field of typeDefinition.fields) { - if (field.directives) { - for (const directive of field.directives) { - const name = directive.name.value; - if (name === 'requires') { - errors.push( - errorWithCode( - 'REQUIRES_USED_ON_BASE', - logServiceAndType( - serviceName, - typeDefinition.name.value, - field.name.value, - ) + - `Found extraneous @requires directive. @requires cannot be used on base types.`, - ), - ); - } - } - } - } - } - }, - }); - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts b/packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts deleted file mode 100644 index 0434f7bd28c..00000000000 --- a/packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { GraphQLError, visit } from 'graphql'; -import { ServiceDefinition } from '../../types'; -import { - logServiceAndType, - errorWithCode, - reservedRootFields -} from '../../utils'; - -/** - * - Schemas should not define the _service or _entitites fields on the query root - */ -export const reservedFieldUsed = ({ - name: serviceName, - typeDefs, -}: ServiceDefinition) => { - const errors: GraphQLError[] = []; - - let rootQueryName = 'Query'; - visit(typeDefs, { - // find the Query type if redefined - OperationTypeDefinition(node) { - if (node.operation === 'query') { - rootQueryName = node.type.name.value; - } - }, - }); - - visit(typeDefs, { - ObjectTypeDefinition(node) { - if (node.name.value === rootQueryName && node.fields) { - for (const field of node.fields) { - const { value: fieldName } = field.name; - if (reservedRootFields.includes(fieldName)) { - errors.push( - errorWithCode( - 'RESERVED_FIELD_USED', - logServiceAndType(serviceName, rootQueryName, fieldName) + - `${fieldName} is a field reserved for federation and can\'t be used at the Query root.`, - ), - ); - } - } - } - }, - }); - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts b/packages/apollo-federation/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts deleted file mode 100644 index 69f46733eb4..00000000000 --- a/packages/apollo-federation/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import gql from 'graphql-tag'; -import { rootFieldUsed as validateRootFieldUsed } from '../'; -import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); - -describe('rootFieldUsed', () => { - it('has no warnings when no schema definition or extension is provided', () => { - const serviceA = { - typeDefs: gql` - type Query { - product: Product - } - - type Product { - sku: String - } - `, - name: 'serviceA', - }; - - const warnings = validateRootFieldUsed(serviceA); - expect(warnings).toHaveLength(0); - }); - - it('has no warnings when a schema definition / extension is provided, when no default root operation type names are used', () => { - const schemaDefinition = { - typeDefs: gql` - schema { - query: RootQuery - } - - type RootQuery { - product: Product - } - - type Product { - sku: String - } - `, - name: 'schemaDefinition', - }; - - const schemaExtension = { - typeDefs: gql` - extend schema { - query: RootQuery - } - - type RootQuery { - product: Product - } - - type Product { - sku: String - } - `, - name: 'schemaExtension', - }; - - const schemaDefinitionWarnings = validateRootFieldUsed(schemaDefinition); - const schemaExtensionWarnings = validateRootFieldUsed(schemaExtension); - - expect(schemaDefinitionWarnings).toEqual([]); - expect(schemaExtensionWarnings).toEqual([]); - }); - - it('warns when a schema definition / extension is provided, as well as a default root type or extension', () => { - const serviceA = { - typeDefs: gql` - schema { - query: RootQuery - } - - type RootQuery { - product: Product - } - - type Product { - sku: String - } - - type Query { - invalidUseOfQuery: Boolean - } - `, - name: 'serviceA', - }; - - const warnings = validateRootFieldUsed(serviceA); - - expect(warnings).toHaveLength(1); - expect(warnings[0].extensions.code).toEqual('ROOT_QUERY_USED'); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "ROOT_QUERY_USED", - "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", - }, - ] - `); - }); - - it('warns against using default operation type names (Query, Mutation, Subscription) when a non-default operation type name is provided in the schema definition', () => { - const serviceA = { - typeDefs: gql` - schema { - mutation: RootMutation - } - - type RootMutation { - updateProduct(sku: ID!): Product - } - - type Mutation { - invalidUseOfMutation: Boolean - } - `, - name: 'serviceA', - }; - - const warnings = validateRootFieldUsed(serviceA); - - expect(warnings).toHaveLength(1); - expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "ROOT_MUTATION_USED", - "message": "[serviceA] Mutation -> Found invalid use of default root operation name \`Mutation\`. \`Mutation\` is disallowed when \`Schema.mutation\` is set to a type other than \`Mutation\`.", - }, - ] - `); - }); - - it("doesn't warn against using default operation type names when no schema definition is provided", () => { - const serviceA = { - typeDefs: gql` - type Query { - validUseOfQuery: Boolean - } - `, - name: 'serviceA', - }; - - const warnings = validateRootFieldUsed(serviceA); - expect(warnings).toHaveLength(0); - }); - - it("doesn't warn against using default operation type names when a schema is defined", () => { - const serviceA = { - typeDefs: gql` - schema { - mutation: Mutation - } - - type Query { - validUseOfQuery: Boolean - } - - type Mutation { - validUseOfMutation: Product - } - `, - name: 'serviceA', - }; - - const warnings = validateRootFieldUsed(serviceA); - expect(warnings).toHaveLength(0); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/preNormalization/index.ts b/packages/apollo-federation/src/composition/validate/preNormalization/index.ts deleted file mode 100644 index 7ff71799492..00000000000 --- a/packages/apollo-federation/src/composition/validate/preNormalization/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { rootFieldUsed } from './rootFieldUsed'; diff --git a/packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts b/packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts deleted file mode 100644 index 1e6bdc13951..00000000000 --- a/packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - GraphQLError, - visit, - ObjectTypeDefinitionNode, - ObjectTypeExtensionNode, -} from 'graphql'; -import { ServiceDefinition, DefaultRootOperationTypeName } from '../../types'; -import { - logServiceAndType, - errorWithCode, - defaultRootOperationNameLookup -} from '../../utils'; - -/** - * - When a schema definition or extension is provided, warn user against using - * default root operation type names for types or extensions - * (Query, Mutation, Subscription) - */ -export const rootFieldUsed = ({ - name: serviceName, - typeDefs, -}: ServiceDefinition) => { - const errors: GraphQLError[] = []; - - // Array of default root operation names - const defaultRootOperationNames = Object.values( - defaultRootOperationNameLookup, - ); - - const disallowedTypeNames: { - [key in DefaultRootOperationTypeName]?: boolean; - } = {}; - - let hasSchemaDefinitionOrExtension = false; - visit(typeDefs, { - OperationTypeDefinition(node) { - // If we find at least one root operation type definition, we know the user has - // specified either a schema definition or extension. - hasSchemaDefinitionOrExtension = true; - - if ( - !defaultRootOperationNames.includes(node.type.name - .value as DefaultRootOperationTypeName) - ) { - disallowedTypeNames[ - defaultRootOperationNameLookup[node.operation] - ] = true; - } - }, - }); - - // If a schema or schema extension is defined, we need to warn for each improper - // usage of default root operation names. The conditions for an improper usage are: - // 1. root operation type is defined as a non-default name (i.e. query: RootQuery) - // 2. the respective default operation type name is used as a regular type - if (hasSchemaDefinitionOrExtension) { - visit(typeDefs, { - ObjectTypeDefinition: visitType, - ObjectTypeExtension: visitType, - }); - - function visitType( - node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, - ) { - if ( - disallowedTypeNames[node.name.value as DefaultRootOperationTypeName] - ) { - const rootOperationName = node.name.value; - errors.push( - errorWithCode( - `ROOT_${rootOperationName.toUpperCase()}_USED`, - logServiceAndType(serviceName, rootOperationName) + - `Found invalid use of default root operation name \`${rootOperationName}\`. \`${rootOperationName}\` is disallowed when \`Schema.${rootOperationName.toLowerCase()}\` is set to a type other than \`${rootOperationName}\`.`, - ), - ); - } - } - } - - return errors; -}; diff --git a/packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingEnums.test.ts b/packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingEnums.test.ts deleted file mode 100644 index 77b86452c09..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingEnums.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { - GraphQLEnumType, - Kind, - DocumentNode, - validate, - GraphQLSchema, - specifiedDirectives, -} from 'graphql'; -import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; -import { composeServices, buildMapsFromServiceList } from '../../../compose'; -import { - astSerializer, - typeSerializer, - selectionSetSerializer, -} from '../../../../snapshotSerializers'; -import { normalizeTypeDefs } from '../../../normalize'; -import federationDirectives from '../../../../directives'; -import { ServiceDefinition } from '../../../types'; -import { MatchingEnums } from '../matchingEnums'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(typeSerializer); -expect.addSnapshotSerializer(selectionSetSerializer); - -// simulate the first half of the composition process -const createDefinitionsDocumentForServices = ( - serviceList: ServiceDefinition[], -): DocumentNode => { - const { typeDefinitionsMap } = buildMapsFromServiceList(serviceList); - return { - kind: Kind.DOCUMENT, - definitions: Object.values(typeDefinitionsMap).flat(), - }; -}; - -describe('matchingEnums', () => { - let schema: GraphQLSchema; - - // create a blank schema for each test - beforeEach(() => { - schema = new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }); - }); - - it('does not error with matching enums across services', () => { - const serviceList = [ - { - typeDefs: gql` - enum ProductCategory { - BED - BATH - } - `, - name: 'serviceA', - }, - - { - typeDefs: gql` - enum ProductCategory { - BED - BATH - } - `, - name: 'serviceB', - }, - ]; - - const definitionsDocument = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); - expect(errors).toHaveLength(0); - }); - - it('errors when enums in separate services dont match', () => { - const serviceList = [ - { - typeDefs: gql` - enum ProductCategory { - BED - BATH - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - enum ProductCategory { - BEYOND - } - `, - name: 'serviceB', - }, - ]; - - const definitionsDocument = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: The \`ProductCategory\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB]], - ] - `); - }); - - it('errors when enums in separate services dont match', () => { - const serviceList = [ - { - typeDefs: gql` - type Query { - products: [Product]! - } - - type Product @key(fields: "sku") { - sku: String! - upc: String! - type: ProductType - } - - enum ProductType { - BOOK - FURNITURE - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - enum ProductType { - FURNITURE - BOOK - DIGITAL - } - `, - name: 'serviceB', - }, - { - typeDefs: gql` - enum ProductType { - FURNITURE - BOOK - DIGITAL - } - `, - name: 'serviceC', - }, - ]; - - const definitionsDocument = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: The \`ProductType\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB, serviceC]], - ] - `); - }); - - it('errors when an enum name is defined as another type in a service', () => { - const serviceList = [ - { - typeDefs: gql` - enum ProductType { - BOOK - FURNITURE - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type ProductType { - id: String - } - `, - name: 'serviceB', - }, - { - typeDefs: gql` - enum ProductType { - FURNITURE - BOOK - DIGITAL - } - `, - name: 'serviceC', - }, - ]; - - const definitionsDocument = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); - expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: [serviceA] ProductType -> ProductType is an enum in [serviceA, serviceC], but not in [serviceB]], - ] - `); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingUnions.test.ts b/packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingUnions.test.ts deleted file mode 100644 index 7a3ec2b7ed1..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/__tests__/matchingUnions.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - GraphQLSchema, - specifiedDirectives, - Kind, - DocumentNode, -} from 'graphql'; -import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; -import { - typeSerializer, - graphqlErrorSerializer, -} from '../../../../snapshotSerializers'; -import { UniqueUnionTypes } from '..'; -import { ServiceDefinition } from '../../../types'; -import { buildMapsFromServiceList } from '../../../compose'; -import federationDirectives from '../../../../directives'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); -expect.addSnapshotSerializer(typeSerializer); - -function createDocumentsForServices( - serviceList: ServiceDefinition[], -): DocumentNode[] { - const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( - serviceList, - ); - return [ - { - kind: Kind.DOCUMENT, - definitions: Object.values(typeDefinitionsMap).flat(), - }, - { - kind: Kind.DOCUMENT, - definitions: Object.values(typeExtensionsMap).flat(), - }, - ]; -} - -describe('MatchingUnions', () => { - let schema: GraphQLSchema; - - // create a blank schema for each test - beforeEach(() => { - schema = new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }); - }); - - it('enforces unique union names on non-identical union types', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - union ProductOrError = Product | Error - - type Error { - code: Int! - message: String! - } - - type Product @key(fields: "sku") { - sku: ID! - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - union ProductOrError = Product - - type Error { - code: Int! - message: String! - } - - extend type Product @key(fields: "sku") { - sku: ID! @external - colors: [String] - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [UniqueUnionTypes]); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", - "message": "[serviceA] ProductOrError -> The union \`ProductOrError\` is defined in services \`serviceA\` and \`serviceB\`, however their types do not match. Union types with the same name must also consist of identical types. The type Error is mismatched.", - } - `); - }); - - it('permits duplicate union names for identical union types', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - union ProductOrError = Product | Error - - type Error { - code: Int! - message: String! - } - - type Product @key(fields: "sku") { - sku: ID! - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - union ProductOrError = Product | Error - - type Error { - code: Int! - message: String! - } - - type Product @key(fields: "sku") { - sku: ID! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [UniqueUnionTypes]); - expect(errors).toHaveLength(0); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts b/packages/apollo-federation/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts deleted file mode 100644 index 7be01981c13..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { - Kind, - DocumentNode, - GraphQLSchema, - specifiedDirectives, - extendSchema, -} from 'graphql'; -import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; -import { buildMapsFromServiceList } from '../../../compose'; -import { - typeSerializer, - graphqlErrorSerializer, -} from '../../../../snapshotSerializers'; -import federationDirectives from '../../../../directives'; -import { ServiceDefinition } from '../../../types'; -import { PossibleTypeExtensions } from '../possibleTypeExtensions'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); -expect.addSnapshotSerializer(typeSerializer); - -// simulate the first half of the composition process -const createDefinitionsDocumentForServices = ( - serviceList: ServiceDefinition[], -): { - definitions: DocumentNode; - extensions: DocumentNode; -} => { - const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( - serviceList, - ); - return { - definitions: { - kind: Kind.DOCUMENT, - definitions: Object.values(typeDefinitionsMap).flat(), - }, - extensions: { - kind: Kind.DOCUMENT, - definitions: Object.values(typeExtensionsMap).flat(), - }, - }; -}; - -describe('PossibleTypeExtensionsType', () => { - let schema: GraphQLSchema; - - // create a blank schema for each test - beforeEach(() => { - schema = new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }); - }); - - it('does not error with matching enums across services', () => { - const serviceList = [ - { - typeDefs: gql` - extend type Product { - sku: ID - } - `, - name: 'serviceA', - }, - - { - typeDefs: gql` - type Product { - id: ID! - } - `, - name: 'serviceB', - }, - ]; - - const { definitions, extensions } = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); - schema = extendSchema(schema, definitions, { assumeValidSDL: true }); - errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); - expect(errors).toHaveLength(0); - }); - - it('errors when there is an extension with no base', () => { - const serviceList = [ - { - typeDefs: gql` - extend type Product { - id: ID! - } - `, - name: 'serviceA', - }, - ]; - - const { definitions, extensions } = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); - schema = extendSchema(schema, definitions, { assumeValidSDL: true }); - errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); - - expect(errors).toHaveLength(1); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTENSION_WITH_NO_BASE", - "message": "[serviceA] Product -> \`Product\` is an extension type, but \`Product\` is not defined in any service", - }, - ] - `); - }); - - it('errors when trying to extend a type with a different `Kind`', () => { - const serviceList = [ - { - typeDefs: gql` - extend type Product { - sku: ID - } - `, - name: 'serviceA', - }, - - { - typeDefs: gql` - input Product { - id: ID! - } - `, - name: 'serviceB', - }, - ]; - - const { definitions, extensions } = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); - schema = extendSchema(schema, definitions, { assumeValidSDL: true }); - errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTENSION_OF_WRONG_KIND", - "message": "[serviceA] Product -> \`Product\` was originally defined as a InputObjectTypeDefinition and can only be extended by a InputObjectTypeExtension. serviceA defines Product as a ObjectTypeExtension", - }, - ] - `); - }); - - it('does not error', () => { - const serviceList = [ - { - typeDefs: gql` - extend interface Product { - name: String - } - extend type Book implements Product { - sku: ID! - name: String - } - `, - name: 'serviceA', - }, - - { - typeDefs: gql` - type Book { - id: ID! - } - - interface Product { - sku: ID! - } - `, - name: 'serviceB', - }, - ]; - - const { definitions, extensions } = createDefinitionsDocumentForServices( - serviceList, - ); - const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); - schema = extendSchema(schema, definitions, { assumeValidSDL: true }); - errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); - schema = extendSchema(schema, extensions, { assumeValidSDL: true }); - - expect(schema.getType('Book')).toMatchInlineSnapshot(` - type Book implements Product { - id: ID! - sku: ID! - name: String - } - `); - expect(errors).toHaveLength(0); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/sdl/__tests__/tsconfig.json b/packages/apollo-federation/src/composition/validate/sdl/__tests__/tsconfig.json deleted file mode 100644 index a6f70de0bfa..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/__tests__/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../../../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [{ "path": "../../../../../" }] -} diff --git a/packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueFieldDefinitionNames.test.ts b/packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueFieldDefinitionNames.test.ts deleted file mode 100644 index cf56c59a838..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueFieldDefinitionNames.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { - GraphQLSchema, - specifiedDirectives, - DocumentNode, - Kind, - extendSchema, -} from 'graphql'; -import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; -import { typeSerializer } from '../../../../snapshotSerializers'; -import { buildMapsFromServiceList } from '../../../compose'; -import federationDirectives from '../../../../directives'; -import { UniqueFieldDefinitionNames } from '..'; -import { ServiceDefinition } from '../../../types'; - -expect.addSnapshotSerializer(typeSerializer); - -// simulate the first half of the composition process -function createDocumentsForServices( - serviceList: ServiceDefinition[], -): DocumentNode[] { - const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( - serviceList, - ); - return [ - { - kind: Kind.DOCUMENT, - definitions: Object.values(typeDefinitionsMap).flat(), - }, - { - kind: Kind.DOCUMENT, - definitions: Object.values(typeExtensionsMap).flat(), - }, - ]; -} - -describe('UniqueFieldDefinitionNames', () => { - let schema: GraphQLSchema; - - // create a blank schema for each test - beforeEach(() => { - schema = new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }); - }); - - describe('enforces unique field names for', () => { - it('object type definitions', () => { - const [definitions, extensions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID! - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - extend type Product { - sku: Int! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueFieldDefinitionNames, - ]); - schema = extendSchema(schema, definitions, { - assumeValidSDL: true, - }); - - errors.push( - ...validateSDL(extensions, schema, [UniqueFieldDefinitionNames]), - ); - - expect(errors).toHaveLength(1); - expect(errors[0].message).toMatch( - 'Field "Product.sku" already exists in the schema.', - ); - }); - - it('interface definitions', () => { - const [definitions, extensions] = createDocumentsForServices([ - { - typeDefs: gql` - interface Product { - sku: ID! - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - extend interface Product { - sku: String! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueFieldDefinitionNames, - ]); - schema = extendSchema(schema, definitions, { assumeValidSDL: true }); - errors.push( - ...validateSDL(extensions, schema, [UniqueFieldDefinitionNames]), - ); - expect(errors).toHaveLength(1); - expect(errors[0].message).toMatch( - 'Field "Product.sku" already exists in the schema.', - ); - }); - - it('input object definitions', () => { - const [definitions, extensions] = createDocumentsForServices([ - { - typeDefs: gql` - input Product { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - extend input Product { - sku: String! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueFieldDefinitionNames, - ]); - schema = extendSchema(schema, definitions, { assumeValidSDL: true }); - errors.push( - ...validateSDL(extensions, schema, [UniqueFieldDefinitionNames]), - ); - expect(errors).toHaveLength(1); - expect(errors[0].message).toMatch( - 'Field "Product.sku" already exists in the schema.', - ); - }); - }); - - describe('permits duplicate field names for', () => { - it('value types (identical object types)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID! - color: String - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - sku: ID! - color: String - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueFieldDefinitionNames, - ]); - expect(errors).toHaveLength(0); - }); - - it('value types (identical interface types)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - interface Product { - sku: ID! - color: String - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - interface Product { - sku: ID! - color: String - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueFieldDefinitionNames, - ]); - expect(errors).toHaveLength(0); - }); - - it('value types (identical input types)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - input Product { - sku: ID! - color: String - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - input Product { - sku: ID! - color: String - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueFieldDefinitionNames, - ]); - expect(errors).toHaveLength(0); - }); - - it('object type definitions (non-identical, value types with type mismatch)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID! - color: String - quantity: Int - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - sku: String! - color: String - quantity: Int! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueFieldDefinitionNames, - ]); - expect(errors).toHaveLength(0); - }); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts deleted file mode 100644 index a56c6c6ae53..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { - GraphQLSchema, - specifiedDirectives, - Kind, - DocumentNode, -} from 'graphql'; -import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; -import { - typeSerializer, - graphqlErrorSerializer, -} from '../../../../snapshotSerializers'; -import federationDirectives from '../../../../directives'; -import { UniqueTypeNamesWithFields } from '..'; -import { ServiceDefinition } from '../../../types'; -import { buildMapsFromServiceList } from '../../../compose'; - -expect.addSnapshotSerializer(graphqlErrorSerializer); -expect.addSnapshotSerializer(typeSerializer); - -function createDocumentsForServices( - serviceList: ServiceDefinition[], -): DocumentNode[] { - const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( - serviceList, - ); - return [ - { - kind: Kind.DOCUMENT, - definitions: Object.values(typeDefinitionsMap).flat(), - }, - { - kind: Kind.DOCUMENT, - definitions: Object.values(typeExtensionsMap).flat(), - }, - ]; -} - -describe('UniqueTypeNamesWithFields', () => { - let schema: GraphQLSchema; - - // create a blank schema for each test - beforeEach(() => { - schema = new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }); - }); - - describe('enforces unique type names for', () => { - it('object type definitions (non-identical, non-value types)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID! - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - color: String! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(1); - expect(errors[0].message).toMatch( - 'There can be only one type named "Product".', - ); - }); - - it('object type definitions (non-identical, value types with type mismatch)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID! - color: String - quantity: Int - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - sku: String! - color: String - quantity: Int! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(2); - expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", - "message": "[serviceA] Product.sku -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.sku\` as a ID! and String! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", - }, - Object { - "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", - "message": "[serviceA] Product.quantity -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.quantity\` as a Int and Int! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", - }, - ] - `); - }); - - it('object type definitions (overlapping fields, but non-value types)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID! - color: String - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - sku: ID! - blah: Int! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(1); - expect(errors[0].message).toMatch( - 'There can be only one type named "Product".', - ); - }); - - it('interface definitions', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - interface Product { - sku: ID! - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - interface Product { - color: String! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(1); - expect(errors[0].message).toMatch( - 'There can be only one type named "Product".', - ); - }); - - it('input definitions', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - input Product { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - input Product { - color: String! - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(1); - expect(errors[0].message).toMatch( - 'There can be only one type named "Product".', - ); - }); - }); - - describe('permits duplicate type names for', () => { - it('scalar types', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - scalar JSON - `, - name: 'serviceA', - }, - { - typeDefs: gql` - scalar JSON - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(0); - }); - - it('enum types (congruency enforced in other validations)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - enum Category { - Furniture - Supplies - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - enum Category { - Things - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(0); - }); - - it('input types', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - input Product { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - input Product { - sku: ID - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(0); - }); - - it('value types (non-entity type definitions that are identical)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - sku: ID - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(0); - }); - }); - - describe('edge cases', () => { - it('value types must be of the same kind', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - input Product { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - sku: ID - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_KIND_MISMATCH", - "message": "[serviceA] Product -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`Product\` is defined as both a \`ObjectTypeDefinition\` and a \`InputObjectTypeDefinition\`. In order to define \`Product\` in multiple places, the kinds must be identical.", - } - `); - }); - - it('value types must be of the same kind (scalar)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - scalar DateTime - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type DateTime { - day: Int - formatted: String - # ... - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_KIND_MISMATCH", - "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`ScalarTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", - } - `); - }); - - it('value types must be of the same kind (union)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - union DateTime = Date | Time - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type DateTime { - day: Int - formatted: String - # ... - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_KIND_MISMATCH", - "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`UnionTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", - } - `); - }); - - it('value types must be of the same kind (enum)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - enum DateTime { - DATE - TIME - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type DateTime { - day: Int - formatted: String - # ... - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_KIND_MISMATCH", - "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`EnumTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", - } - `); - }); - - it('value types cannot be entities (part 1)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product { - sku: ID - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_NO_ENTITY", - "message": "[serviceA] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", - } - `); - }); - - it('value types cannot be entities (part 2)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: ID - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchInlineSnapshot(` - Object { - "code": "VALUE_TYPE_NO_ENTITY", - "message": "[serviceB] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", - } - `); - }); - - it('no false positives for properly formed entities (that look like value types)', () => { - const [definitions] = createDocumentsForServices([ - { - typeDefs: gql` - type Product @key(fields: "sku") { - sku: ID - } - `, - name: 'serviceA', - }, - { - typeDefs: gql` - extend type Product @key(fields: "sku") { - sku: ID @external - } - `, - name: 'serviceB', - }, - ]); - - const errors = validateSDL(definitions, schema, [ - UniqueTypeNamesWithFields, - ]); - expect(errors).toHaveLength(0); - }); - }); -}); diff --git a/packages/apollo-federation/src/composition/validate/sdl/index.ts b/packages/apollo-federation/src/composition/validate/sdl/index.ts deleted file mode 100644 index 5a3ed576233..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { UniqueTypeNamesWithFields } from './uniqueTypeNamesWithFields'; -export { MatchingEnums } from './matchingEnums'; -export { PossibleTypeExtensions } from './possibleTypeExtensions'; -export { UniqueFieldDefinitionNames } from './uniqueFieldDefinitionNames'; -export { UniqueUnionTypes } from './matchingUnions'; diff --git a/packages/apollo-federation/src/composition/validate/sdl/matchingEnums.ts b/packages/apollo-federation/src/composition/validate/sdl/matchingEnums.ts deleted file mode 100644 index bb162963a3e..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/matchingEnums.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { SDLValidationContext } from 'graphql/validation/ValidationContext'; -import { - ASTVisitor, - Kind, - EnumTypeDefinitionNode, - EnumValueDefinitionNode, - TypeDefinitionNode, -} from 'graphql'; -import { errorWithCode, logServiceAndType } from '../../utils'; -import { isString } from 'util'; - -function isEnumDefinition(node: TypeDefinitionNode) { - return node.kind === Kind.ENUM_TYPE_DEFINITION; -} - -type TypeToDefinitionsMap = { - [typeNems: string]: TypeDefinitionNode[]; -}; - -export function MatchingEnums(context: SDLValidationContext): ASTVisitor { - const { definitions } = context.getDocument(); - - // group all definitions by name - // { MyTypeName: [{ serviceName: "A", name: {...}}]} - let definitionsByName: { - [typeName: string]: TypeDefinitionNode[]; - } = (definitions as TypeDefinitionNode[]).reduce( - (typeToDefinitionsMap: TypeToDefinitionsMap, node) => { - const name = node.name.value; - if (typeToDefinitionsMap[name]) { - typeToDefinitionsMap[name].push(node); - } else { - typeToDefinitionsMap[name] = [node]; - } - return typeToDefinitionsMap; - }, - {}, - ); - - // map over each group of definitions. - for (const [name, definitions] of Object.entries(definitionsByName)) { - // if every definition in the list is an enum, we don't need to error about type, - // but we do need to check to make sure every service has the same enum values - if (definitions.every(isEnumDefinition)) { - // a simple list of services to enum values for a given enum - // [{ serviceName: "serviceA", values: ["FURNITURE", "BOOK"] }] - let simpleEnumDefs: Array<{ serviceName: string; values: string[] }> = []; - - // build the simpleEnumDefs list - for (const { - values, - serviceName, - } of definitions as EnumTypeDefinitionNode[]) { - if (serviceName && values) - simpleEnumDefs.push({ - serviceName, - values: values.map( - (enumValue: EnumValueDefinitionNode) => enumValue.name.value, - ), - }); - } - - // values need to be in order to build the matchingEnumGroups - for (const definition of simpleEnumDefs) { - definition.values = definition.values.sort(); - } - - // groups of services with matching values, keyed by enum values - // like {"FURNITURE,BOOK": ["ServiceA", "ServiceB"], "FURNITURE,DIGITAL": ["serviceC"]} - let matchingEnumGroups: { [values: string]: string[] } = {}; - - // build matchingEnumDefs - for (const definition of simpleEnumDefs) { - const key = definition.values.join(); - if (matchingEnumGroups[key]) { - matchingEnumGroups[key].push(definition.serviceName); - } else { - matchingEnumGroups[key] = [definition.serviceName]; - } - } - - if (Object.keys(matchingEnumGroups).length > 1) { - context.reportError( - errorWithCode( - 'ENUM_MISMATCH', - `The \`${name}\` enum does not have identical values in all services. Groups of services with identical values are: ${Object.values( - matchingEnumGroups, - ) - .map(serviceNames => `[${serviceNames.join(', ')}]`) - .join(', ')}`, - ), - ); - } - } else if (definitions.some(isEnumDefinition)) { - // if only SOME definitions in the list are enums, we need to error - - // first, find the services, where the defs ARE enums - const servicesWithEnum = definitions - .filter(isEnumDefinition) - .map(definition => definition.serviceName) - .filter(isString); - - // find the services where the def isn't an enum - const servicesWithoutEnum = definitions - .filter(d => !isEnumDefinition(d)) - .map(d => d.serviceName) - .filter(isString); - - context.reportError( - errorWithCode( - 'ENUM_MISMATCH_TYPE', - logServiceAndType(servicesWithEnum[0], name) + - `${name} is an enum in [${servicesWithEnum.join( - ', ', - )}], but not in [${servicesWithoutEnum.join(', ')}]`, - ), - ); - } - } - - // we don't need any visitors for this validation rule - return {}; -} diff --git a/packages/apollo-federation/src/composition/validate/sdl/matchingUnions.ts b/packages/apollo-federation/src/composition/validate/sdl/matchingUnions.ts deleted file mode 100644 index 7a74fb833c8..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/matchingUnions.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { GraphQLError, ASTVisitor, UnionTypeDefinitionNode } from 'graphql'; -import { SDLValidationContext } from 'graphql/validation/ValidationContext'; -import xorBy from 'lodash.xorby'; -import { Maybe } from '../../types'; -import { errorWithCode, logServiceAndType } from '../../utils'; -import { - existedTypeNameMessage, - duplicateTypeNameMessage, -} from './uniqueTypeNamesWithFields'; - -/** - * Unique type names - * A GraphQL document is only valid if all defined types have unique names. - * Modified to allow duplicate enum and scalar names - */ -export function UniqueUnionTypes(context: SDLValidationContext): ASTVisitor { - const knownTypes: { - [typeName: string]: UnionTypeDefinitionNode; - } = Object.create(null); - const schema = context.getSchema(); - - return { - UnionTypeDefinition: validateUnionTypes, - }; - - function validateUnionTypes(node: UnionTypeDefinitionNode) { - const typeName = node.name.value; - const typeFromSchema = schema && schema.getType(typeName); - const typeNodeFromSchema = - typeFromSchema && - (typeFromSchema.astNode as Maybe); - - const typeNodeFromDefs = knownTypes[typeName]; - const duplicateTypeNode = typeNodeFromSchema || typeNodeFromDefs; - - // Exception for identical union types - if (duplicateTypeNode) { - const unionDiff = xorBy( - node.types, - duplicateTypeNode.types, - 'name.value', - ); - - const diffLength = unionDiff.length; - if (diffLength > 0) { - context.reportError( - errorWithCode( - 'VALUE_TYPE_UNION_TYPES_MISMATCH', - `${logServiceAndType( - duplicateTypeNode.serviceName!, - typeName, - )}The union \`${typeName}\` is defined in services \`${ - duplicateTypeNode.serviceName - }\` and \`${ - node.serviceName - }\`, however their types do not match. Union types with the same name must also consist of identical types. The type${ - diffLength > 1 ? 's' : '' - } ${unionDiff.map(diffEntry => diffEntry.name.value).join(', ')} ${ - diffLength > 1 ? 'are' : 'is' - } mismatched.`, - [node, duplicateTypeNode], - ), - ); - } - - return false; - } - - if (typeFromSchema) { - context.reportError( - new GraphQLError(existedTypeNameMessage(typeName), node.name), - ); - return; - } - - if (knownTypes[typeName]) { - context.reportError( - new GraphQLError(duplicateTypeNameMessage(typeName), [ - knownTypes[typeName], - node.name, - ]), - ); - } else { - knownTypes[typeName] = node; - } - - return false; - } -} diff --git a/packages/apollo-federation/src/composition/validate/sdl/possibleTypeExtensions.ts b/packages/apollo-federation/src/composition/validate/sdl/possibleTypeExtensions.ts deleted file mode 100644 index f7e37850a02..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/possibleTypeExtensions.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { SDLValidationContext } from 'graphql/validation/ValidationContext'; -import { - ASTVisitor, - isObjectType, - isScalarType, - isInterfaceType, - isUnionType, - isEnumType, - isInputObjectType, - Kind, - isTypeDefinitionNode, - ObjectTypeExtensionNode, - InterfaceTypeExtensionNode, - GraphQLNamedType, -} from 'graphql'; -import { - errorWithCode, - logServiceAndType, - defKindToExtKind, -} from '../../utils'; - -type FederatedExtensionNode = ( - | ObjectTypeExtensionNode - | InterfaceTypeExtensionNode) & { - serviceName?: string | null; -}; - -// This is a variant of the PossibleTypeExtensions validator in graphql-js. -// it was modified to only check object/interface extensions. A custom error -// message was also added. -// original here: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/PossibleTypeExtensions.js -export function PossibleTypeExtensions( - context: SDLValidationContext, -): ASTVisitor { - const schema = context.getSchema(); - const definedTypes = Object.create(null); - - for (const def of context.getDocument().definitions) { - if (isTypeDefinitionNode(def)) { - definedTypes[def.name.value] = def; - } - } - - const checkExtension = (node: FederatedExtensionNode) => { - const typeName = node.name.value; - const defNode = definedTypes[typeName]; - const existingType = schema && schema.getType(typeName); - - const serviceName = node.serviceName; - if (!serviceName) return; - - if (defNode) { - const expectedKind = defKindToExtKind[defNode.kind]; - const baseKind = defNode.kind; - if (expectedKind !== node.kind) { - context.reportError( - errorWithCode( - 'EXTENSION_OF_WRONG_KIND', - logServiceAndType(serviceName, typeName) + - `\`${typeName}\` was originally defined as a ${baseKind} and can only be extended by a ${expectedKind}. ${serviceName} defines ${typeName} as a ${node.kind}`, - ), - ); - } - } else if (existingType) { - const expectedKind = typeToExtKind(existingType); - const baseKind = typeToKind(existingType); - if (expectedKind !== node.kind) { - context.reportError( - errorWithCode( - 'EXTENSION_OF_WRONG_KIND', - logServiceAndType(serviceName, typeName) + - `\`${typeName}\` was originally defined as a ${baseKind} and can only be extended by a ${expectedKind}. ${serviceName} defines ${typeName} as a ${node.kind}`, - ), - ); - } - } else { - context.reportError( - errorWithCode( - 'EXTENSION_WITH_NO_BASE', - logServiceAndType(serviceName, typeName) + - `\`${typeName}\` is an extension type, but \`${typeName}\` is not defined in any service`, - ), - ); - } - }; - - return { - ObjectTypeExtension: checkExtension, - InterfaceTypeExtension: checkExtension, - }; -} - -// These following utility functions/objects are part of the -// PossibleTypeExtensions validations in graphql-js, but not exported. -// https://github.com/graphql/graphql-js/blob/d8c1dfdc9dbbdef2400363cb0748d50cbeef39a8/src/validation/rules/PossibleTypeExtensions.js#L110 -function typeToExtKind(type: GraphQLNamedType) { - if (isScalarType(type)) { - return Kind.SCALAR_TYPE_EXTENSION; - } else if (isObjectType(type)) { - return Kind.OBJECT_TYPE_EXTENSION; - } else if (isInterfaceType(type)) { - return Kind.INTERFACE_TYPE_EXTENSION; - } else if (isUnionType(type)) { - return Kind.UNION_TYPE_EXTENSION; - } else if (isEnumType(type)) { - return Kind.ENUM_TYPE_EXTENSION; - } else if (isInputObjectType(type)) { - return Kind.INPUT_OBJECT_TYPE_EXTENSION; - } - return null; -} - -// this function is purely for printing out the `Kind` of the base type def. -function typeToKind(type: GraphQLNamedType) { - if (isScalarType(type)) { - return Kind.SCALAR_TYPE_DEFINITION; - } else if (isObjectType(type)) { - return Kind.OBJECT_TYPE_DEFINITION; - } else if (isInterfaceType(type)) { - return Kind.INTERFACE_TYPE_DEFINITION; - } else if (isUnionType(type)) { - return Kind.UNION_TYPE_DEFINITION; - } else if (isEnumType(type)) { - return Kind.ENUM_TYPE_DEFINITION; - } else if (isInputObjectType(type)) { - return Kind.INPUT_OBJECT_TYPE_DEFINITION; - } - return null; -} diff --git a/packages/apollo-federation/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts b/packages/apollo-federation/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts deleted file mode 100644 index 449174184c2..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { - ASTVisitor, - NameNode, - GraphQLError, - InputObjectTypeDefinitionNode, - InterfaceTypeDefinitionNode, - ObjectTypeDefinitionNode, - InputObjectTypeExtensionNode, - InterfaceTypeExtensionNode, - ObjectTypeExtensionNode, - GraphQLNamedType, - isObjectType, - isInterfaceType, - isInputObjectType, -} from 'graphql'; -import { SDLValidationContext } from 'graphql/validation/ValidationContext'; -import { TypeMap } from 'graphql/type/schema'; -import { Maybe } from '../../types'; -import { diffTypeNodes, logServiceAndType } from '../../utils'; - -type TypeNodeWithFields = TypeDefinitionWithFields | TypeExtensionWithFields; - -type TypeDefinitionWithFields = - | InputObjectTypeDefinitionNode - | InterfaceTypeDefinitionNode - | ObjectTypeDefinitionNode; - -type TypeExtensionWithFields = - | InputObjectTypeExtensionNode - | InterfaceTypeExtensionNode - | ObjectTypeExtensionNode; - -export function duplicateFieldDefinitionNameMessage( - typeName: string, - fieldName: string, -): string { - return `Field "${typeName}.${fieldName}" can only be defined once.`; -} - -export function existedFieldDefinitionNameMessage( - typeName: string, - fieldName: string, - serviceName: string, -): string { - return `${logServiceAndType( - serviceName, - typeName, - fieldName, - )}Field "${typeName}.${fieldName}" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.`; -} - -/** - * Unique field definition names - * - * A GraphQL complex type is only valid if all its fields are uniquely named. - * Modified to permit duplicate field names on value types. - */ -export function UniqueFieldDefinitionNames( - context: SDLValidationContext, -): ASTVisitor { - const schema = context.getSchema(); - const existingTypeMap: TypeMap = schema - ? schema.getTypeMap() - : Object.create(null); - interface FieldToNameNodeMap { - [fieldName: string]: NameNode; - } - const knownFieldNames: { - [typeName: string]: FieldToNameNodeMap; - } = Object.create(null); - - const possibleValueTypes: { - [key: string]: TypeNodeWithFields | undefined; - } = Object.create(null); - - // Maintain original functionality for type extensions, but substitute our - // more permissive validator for base types to allow value types - return { - InputObjectTypeExtension: checkFieldUniqueness, - InterfaceTypeExtension: checkFieldUniqueness, - ObjectTypeExtension: checkFieldUniqueness, - InputObjectTypeDefinition: checkFieldUniquenessExcludingValueTypes, - InterfaceTypeDefinition: checkFieldUniquenessExcludingValueTypes, - ObjectTypeDefinition: checkFieldUniquenessExcludingValueTypes, - }; - - function checkFieldUniqueness(node: TypeExtensionWithFields) { - const typeName = node.name.value; - - if (!knownFieldNames[typeName]) { - knownFieldNames[typeName] = Object.create(null); - } - - if (!node.fields) { - return false; - } - - const fieldNames = knownFieldNames[typeName]; - - for (const fieldDef of node.fields) { - const fieldName = fieldDef.name.value; - - if (hasField(existingTypeMap[typeName], fieldName)) { - context.reportError( - new GraphQLError( - existedFieldDefinitionNameMessage( - typeName, - fieldName, - existingTypeMap[typeName].astNode!.serviceName!, - ), - fieldDef.name, - ), - ); - } else if (fieldNames[fieldName]) { - context.reportError( - new GraphQLError( - duplicateFieldDefinitionNameMessage(typeName, fieldName), - [fieldNames[fieldName], fieldDef.name], - ), - ); - } else { - fieldNames[fieldName] = fieldDef.name; - } - } - - return false; - } - - /** - * Similar to checkFieldUniqueness above, with some extra permissions: - * - * 1) Non-uniqueness *on value types* (same field names, same field types) should be permitted - * 2) *Near* value types are also permitted here (with relevant errors in uniqueTypeNamesWithFields) - * - Near value types share only the same type name and field names. Permitting these cases allows - * us to catch and warn on likely user errors. - * - * @param node TypeDefinitionWithFields - */ - function checkFieldUniquenessExcludingValueTypes( - node: TypeDefinitionWithFields, - ) { - const typeName = node.name.value; - - const valueTypeFromSchema = - existingTypeMap[typeName] && - (existingTypeMap[typeName].astNode as Maybe); - const duplicateTypeNode = - valueTypeFromSchema || possibleValueTypes[node.name.value]; - - if (duplicateTypeNode) { - const { fields } = diffTypeNodes(node, duplicateTypeNode); - - // This is the condition required for a *near* value type. At this point, we know the - // parent type names are the same. We know the field names are the same if either: - // 1) the field has no entry in the fields diff (they're identical), or - // 2) the field's diff entry is an array of length 2 (both nodes have the field, but the field types are different) - if (Object.values(fields).every(diffEntry => diffEntry.length === 2)) { - return false; - } - } else { - possibleValueTypes[node.name.value] = node; - } - - if (!knownFieldNames[typeName]) { - knownFieldNames[typeName] = Object.create(null); - } - - if (!node.fields) { - return false; - } - - const fieldNames = knownFieldNames[typeName]; - - for (const fieldDef of node.fields) { - const fieldName = fieldDef.name.value; - if (hasField(existingTypeMap[typeName], fieldName)) { - context.reportError( - new GraphQLError( - existedFieldDefinitionNameMessage( - typeName, - fieldName, - existingTypeMap[typeName].astNode!.serviceName!, - ), - fieldDef.name, - ), - ); - } else if (fieldNames[fieldName]) { - context.reportError( - new GraphQLError( - duplicateFieldDefinitionNameMessage(typeName, fieldName), - [fieldNames[fieldName], fieldDef.name], - ), - ); - } else { - fieldNames[fieldName] = fieldDef.name; - } - } - - return false; - } -} - -function hasField(type: GraphQLNamedType, fieldName: string) { - if (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type)) { - return Boolean(type.getFields()[fieldName]); - } - return false; -} diff --git a/packages/apollo-federation/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts b/packages/apollo-federation/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts deleted file mode 100644 index 5615c6e62a0..00000000000 --- a/packages/apollo-federation/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - GraphQLError, - ASTVisitor, - TypeDefinitionNode, -} from 'graphql'; - -import { SDLValidationContext } from 'graphql/validation/ValidationContext'; -import { Maybe } from '../../types'; -import { - isTypeNodeAnEntity, - diffTypeNodes, - errorWithCode, - logServiceAndType, -} from '../../utils'; - -export function duplicateTypeNameMessage(typeName: string): string { - return `There can be only one type named "${typeName}".`; -} - -export function existedTypeNameMessage(typeName: string): string { - return `Type "${typeName}" already exists in the schema. It cannot also be defined in this type definition.`; -} - -/** - * Unique type names - * A GraphQL document is only valid if all defined types have unique names. - * Modified to allow duplicate enum and scalar names - */ -export function UniqueTypeNamesWithFields( - context: SDLValidationContext, -): ASTVisitor { - const knownTypes: { - [typeName: string]: TypeDefinitionNode; - } = Object.create(null); - const schema = context.getSchema(); - - return { - ScalarTypeDefinition: checkTypeName, - ObjectTypeDefinition: checkTypeName, - InterfaceTypeDefinition: checkTypeName, - UnionTypeDefinition: checkTypeName, - EnumTypeDefinition: checkTypeName, - InputObjectTypeDefinition: checkTypeName, - }; - - function checkTypeName(node: TypeDefinitionNode) { - const typeName = node.name.value; - const typeFromSchema = schema && schema.getType(typeName); - const typeNodeFromSchema = - typeFromSchema && - (typeFromSchema.astNode as Maybe); - - const typeNodeFromDefs = knownTypes[typeName]; - const duplicateTypeNode = typeNodeFromSchema || typeNodeFromDefs; - - /* - * Return early for value types - * Value types: - * 1) have the same kind (type, interface, input), extensions are excluded - * 2) are not entities - * 3) have the same set of fields - */ - if (duplicateTypeNode) { - const possibleErrors: GraphQLError[] = []; - // By inspecting the diff, we can warn when field types mismatch. - // A diff entry will exist when a field exists on one type and not the other, or if there is a type mismatch on the field - // i.e. { sku: [Int, String!], color: [String] } - const { kind, fields } = diffTypeNodes(node, duplicateTypeNode); - - const fieldsDiff = Object.entries(fields); - - // Error if the kinds don't match - if (kind.length > 0) { - context.reportError( - errorWithCode( - 'VALUE_TYPE_KIND_MISMATCH', - `${logServiceAndType( - duplicateTypeNode.serviceName!, - typeName, - )}Found kind mismatch on expected value type belonging to services \`${ - duplicateTypeNode.serviceName - }\` and \`${ - node.serviceName - }\`. \`${typeName}\` is defined as both a \`${ - kind[0] - }\` and a \`${ - kind[1] - }\`. In order to define \`${typeName}\` in multiple places, the kinds must be identical.`, - [node, duplicateTypeNode], - ), - ); - return; - } - - const typesHaveSameShape = - fieldsDiff.length === 0 || - fieldsDiff.every(([fieldName, types]) => { - // If a diff entry has two types, then the field name matches but the types do not. - // In this case, we can push a useful error to hint to the user that we - // think they tried to define a value type, but one of the fields has a type mismatch. - if (types.length === 2) { - possibleErrors.push( - errorWithCode( - 'VALUE_TYPE_FIELD_TYPE_MISMATCH', - `${logServiceAndType( - duplicateTypeNode.serviceName!, - typeName, - fieldName, - )}A field was defined differently in different services. \`${ - duplicateTypeNode.serviceName - }\` and \`${ - node.serviceName - }\` define \`${typeName}.${fieldName}\` as a ${types[1]} and ${ - types[0] - } respectively. In order to define \`${typeName}\` in multiple places, the fields and their types must be identical.`, - [node, duplicateTypeNode], - ), - ); - return true; - } - return false; - }); - - // Once we determined that types have the same shape (name, kind, and field - // names), we can provide useful errors - if (typesHaveSameShape) { - // Report errors that were collected while determining the matching shape of the types - possibleErrors.forEach(error => context.reportError(error)); - - // Error if either is an entity - if (isTypeNodeAnEntity(node) || isTypeNodeAnEntity(duplicateTypeNode)) { - const entityNode = isTypeNodeAnEntity(duplicateTypeNode) - ? duplicateTypeNode - : node; - - context.reportError( - errorWithCode( - 'VALUE_TYPE_NO_ENTITY', - `${logServiceAndType( - entityNode.serviceName!, - typeName, - )}Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`${typeName}\` type is extended properly or remove the \`@key\` directive if this is not an entity.`, - [node, duplicateTypeNode], - ), - ); - } - - return false; - } - } - - if (typeFromSchema) { - context.reportError( - new GraphQLError(existedTypeNameMessage(typeName), node.name), - ); - return; - } - - if (knownTypes[typeName]) { - context.reportError( - new GraphQLError(duplicateTypeNameMessage(typeName), [ - knownTypes[typeName], - node.name, - ]), - ); - } else { - knownTypes[typeName] = node; - } - - return false; - } -} diff --git a/packages/apollo-federation/src/csdlDirectives.ts b/packages/apollo-federation/src/csdlDirectives.ts deleted file mode 100644 index 4a3fd103e9b..00000000000 --- a/packages/apollo-federation/src/csdlDirectives.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - GraphQLDirective, - DirectiveLocation, - GraphQLNonNull, - GraphQLString, - GraphQLInt, -} from 'graphql'; - -export const ComposedGraphDirective = new GraphQLDirective({ - name: 'composedGraph', - locations: [DirectiveLocation.SCHEMA], - args: { - version: { - type: GraphQLNonNull(GraphQLInt), - }, - }, -}); - -export const GraphDirective = new GraphQLDirective({ - name: 'graph', - locations: [DirectiveLocation.SCHEMA], - args: { - name: { - type: GraphQLNonNull(GraphQLString), - }, - url: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const OwnerDirective = new GraphQLDirective({ - name: 'owner', - locations: [DirectiveLocation.OBJECT], - args: { - graph: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const KeyDirective = new GraphQLDirective({ - name: 'key', - locations: [DirectiveLocation.OBJECT], - args: { - fields: { - type: GraphQLNonNull(GraphQLString), - }, - graph: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const ResolveDirective = new GraphQLDirective({ - name: 'resolve', - locations: [DirectiveLocation.FIELD_DEFINITION], - args: { - graph: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const ProvidesDirective = new GraphQLDirective({ - name: 'provides', - locations: [DirectiveLocation.FIELD_DEFINITION], - args: { - fields: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const RequiresDirective = new GraphQLDirective({ - name: 'requires', - locations: [DirectiveLocation.FIELD_DEFINITION], - args: { - fields: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const csdlDirectives = [ - ComposedGraphDirective, - GraphDirective, - OwnerDirective, - KeyDirective, - ResolveDirective, - ProvidesDirective, - RequiresDirective, -]; - -export default csdlDirectives; diff --git a/packages/apollo-federation/src/directives.ts b/packages/apollo-federation/src/directives.ts deleted file mode 100644 index dfd723c002e..00000000000 --- a/packages/apollo-federation/src/directives.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - GraphQLDirective, - DirectiveLocation, - GraphQLNonNull, - GraphQLString, - GraphQLNamedType, - isInputObjectType, - GraphQLInputObjectType, - DirectiveNode, - ScalarTypeDefinitionNode, - ObjectTypeDefinitionNode, - InterfaceTypeDefinitionNode, - UnionTypeDefinitionNode, - EnumTypeDefinitionNode, - ScalarTypeExtensionNode, - ObjectTypeExtensionNode, - InterfaceTypeExtensionNode, - UnionTypeExtensionNode, - EnumTypeExtensionNode, - GraphQLField, - FieldDefinitionNode, -} from 'graphql'; - -export const KeyDirective = new GraphQLDirective({ - name: 'key', - locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], - args: { - fields: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const ExtendsDirective = new GraphQLDirective({ - name: 'extends', - locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], -}); - -export const ExternalDirective = new GraphQLDirective({ - name: 'external', - locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION], -}); - -export const RequiresDirective = new GraphQLDirective({ - name: 'requires', - locations: [DirectiveLocation.FIELD_DEFINITION], - args: { - fields: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const ProvidesDirective = new GraphQLDirective({ - name: 'provides', - locations: [DirectiveLocation.FIELD_DEFINITION], - args: { - fields: { - type: GraphQLNonNull(GraphQLString), - }, - }, -}); - -export const federationDirectives = [ - KeyDirective, - ExtendsDirective, - ExternalDirective, - RequiresDirective, - ProvidesDirective, -]; - -export default federationDirectives; - -export type ASTNodeWithDirectives = - | ScalarTypeDefinitionNode - | ObjectTypeDefinitionNode - | InterfaceTypeDefinitionNode - | UnionTypeDefinitionNode - | EnumTypeDefinitionNode - | ScalarTypeExtensionNode - | ObjectTypeExtensionNode - | InterfaceTypeExtensionNode - | UnionTypeExtensionNode - | EnumTypeExtensionNode - | FieldDefinitionNode; - -// | GraphQLField -export type GraphQLNamedTypeWithDirectives = Exclude< - GraphQLNamedType, - GraphQLInputObjectType ->; - -function hasDirectives( - node: ASTNodeWithDirectives, -): node is ASTNodeWithDirectives & { - directives: ReadonlyArray; -} { - return Boolean('directives' in node && node.directives); -} - -export function gatherDirectives( - type: GraphQLNamedTypeWithDirectives | GraphQLField, -): DirectiveNode[] { - let directives: DirectiveNode[] = []; - if ('extensionASTNodes' in type && type.extensionASTNodes) { - for (const node of type.extensionASTNodes) { - if (hasDirectives(node)) { - directives = directives.concat(node.directives); - } - } - } - - if (type.astNode && hasDirectives(type.astNode)) - directives = directives.concat(type.astNode.directives); - - return directives; -} - -export function typeIncludesDirective( - type: GraphQLNamedType, - directiveName: string, -): boolean { - if (isInputObjectType(type)) return false; - const directives = gatherDirectives(type as GraphQLNamedTypeWithDirectives); - return directives.some(directive => directive.name.value === directiveName); -} diff --git a/packages/apollo-federation/src/index.ts b/packages/apollo-federation/src/index.ts deleted file mode 100644 index 036f8a0ad75..00000000000 --- a/packages/apollo-federation/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "core-js/features/array/flat"; -import "core-js/features/array/flat-map"; - -export * from './composition'; -export * from './service'; diff --git a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts deleted file mode 100644 index 3693b1b5aab..00000000000 --- a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts +++ /dev/null @@ -1,627 +0,0 @@ -import gql from 'graphql-tag'; -import { Kind, graphql, DocumentNode, execute } from 'graphql'; -import { buildFederatedSchema } from '../buildFederatedSchema'; -import { typeSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(typeSerializer); - -const EMPTY_DOCUMENT = { - kind: Kind.DOCUMENT, - definitions: [], -}; - -describe('buildFederatedSchema', () => { - it(`should mark a type with a key field as an entity`, () => { - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` -type Product { - upc: String! - name: String - price: Int -} -`); - - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - }); - - it(`should mark a type with multiple key fields as an entity`, () => { - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") @key(fields: "sku") { - upc: String! - sku: String! - name: String - price: Int - } - `); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` -type Product { - upc: String! - sku: String! - name: String - price: Int -} -`); - - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - }); - - it(`should not mark a type without a key field as an entity`, () => { - const schema = buildFederatedSchema(gql` - type Money { - amount: Int! - currencyCode: String! - } - `); - - expect(schema.getType('Money')).toMatchInlineSnapshot(` -type Money { - amount: Int! - currencyCode: String! -} -`); - }); - - it('should preserve description text in generated SDL', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` - "A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text" - type User @key(fields: "id") { - """ - The unique ID of the user. - """ - id: ID! - "The user's name." - name: String - username: String - foo( - "Description 1" - arg1: String - "Description 2" - arg2: String - "Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3" - arg3: String - ): String - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data?._service.sdl).toEqual(`""" -A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text -""" -type User @key(fields: "id") { - """The unique ID of the user.""" - id: ID! - - """The user's name.""" - name: String - username: String - foo( - """Description 1""" - arg1: String - - """Description 2""" - arg2: String - - """ - Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 - """ - arg3: String - ): String -} -`); - }); - - describe(`should add an _entities query root field to the schema`, () => { - it(`when a query root type with the default name has been defined`, () => { - const schema = buildFederatedSchema(gql` - type Query { - rootField: String - } - type Product @key(fields: "upc") { - upc: ID! - } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` -type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - rootField: String -} -`); - }); - - it(`when a query root type with a non-default name has been defined`, () => { - const schema = buildFederatedSchema(gql` - schema { - query: QueryRoot - } - - type QueryRoot { - rootField: String - } - type Product @key(fields: "upc") { - upc: ID! - } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` -type QueryRoot { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - rootField: String -} -`); - }); - }); - describe(`should not add an _entities query root field to the schema`, () => { - it(`when no query root type has been defined`, () => { - const schema = buildFederatedSchema(EMPTY_DOCUMENT); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` -type Query { - _service: _Service! -} -`); - }); - it(`when no types with keys are found`, () => { - const schema = buildFederatedSchema(gql` - type Query { - rootField: String - } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` -type Query { - _service: _Service! - rootField: String -} -`); - }); - it(`when only an interface with keys are found`, () => { - const schema = buildFederatedSchema(gql` - type Query { - rootField: String - } - interface Product @key(fields: "upc") { - upc: ID! - } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` -type Query { - _service: _Service! - rootField: String -} -`); - }); - }); - describe('_entities root field', () => { - it('executes resolveReference for a type if found', async () => { - const query = `query GetEntities($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - name - } - ... on User { - firstName - } - } - }`; - - const variables = { - representations: [ - { __typename: 'Product', upc: 1 }, - { __typename: 'User', id: 1 }, - ], - }; - - const schema = buildFederatedSchema([ - { - typeDefs: gql` - type Product @key(fields: "upc") { - upc: Int - name: String - } - type User @key(fields: "id") { - firstName: String - } - `, - resolvers: { - Product: { - __resolveReference(object) { - expect(object.upc).toEqual(1); - return { name: 'Apollo Gateway' }; - }, - }, - User: { - __resolveReference(object) { - expect(object.id).toEqual(1); - return Promise.resolve({ firstName: 'James' }); - }, - }, - }, - }, - ]); - const { data, errors } = await graphql( - schema, - query, - null, - null, - variables, - ); - expect(errors).toBeUndefined(); - expect(data._entities[0].name).toEqual('Apollo Gateway'); - expect(data._entities[1].firstName).toEqual('James'); - }); - it('executes resolveReference with default representation values', async () => { - const query = `query GetEntities($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - upc - name - } - } - }`; - - const variables = { - representations: [ - { __typename: 'Product', upc: 1, name: 'Apollo Gateway' }, - ], - }; - - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: Int - name: String - } - `); - const { data, errors } = await graphql( - schema, - query, - null, - null, - variables, - ); - expect(errors).toBeUndefined(); - expect(data._entities[0].name).toEqual('Apollo Gateway'); - }); - }); - describe('_service root field', () => { - it('keeps extension types when owner type is not present', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` - type Review { - id: ID - } - - extend type Review { - title: String - } - - extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl) - .toEqual(`extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] -} - -type Review { - id: ID - title: String -} -`); - }); - it('keeps extension interface when owner interface is not present', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` - type Review { - id: ID - } - - extend type Review { - title: String - } - - interface Node @key(fields: "id") { - id: ID! - } - - extend interface Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl).toEqual(`interface Node @key(fields: "id") { - id: ID! -} - -extend interface Product @key(fields: "upc") { - upc: String @external - reviews: [Review] -} - -type Review { - id: ID - title: String -} -`); - }); - it('returns valid sdl for @key directives', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl).toEqual(`type Product @key(fields: "upc") { - upc: String! - name: String - price: Int -} -`); - }); - it('returns valid sdl for multiple @key directives', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") @key(fields: "name") { - upc: String! - name: String - price: Int - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl) - .toEqual(`type Product @key(fields: "upc") @key(fields: "name") { - upc: String! - name: String - price: Int -} -`); - }); - it('supports all federation directives', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - - const schema = buildFederatedSchema(gql` - type Review @key(fields: "id") { - id: ID! - body: String - author: User @provides(fields: "email") - product: Product @provides(fields: "upc") - } - - extend type User @key(fields: "email") { - email: String @external - reviews: [Review] - } - - extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl) - .toEqual(`extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] -} - -type Review @key(fields: "id") { - id: ID! - body: String - author: User @provides(fields: "email") - product: Product @provides(fields: "upc") -} - -extend type User @key(fields: "email") { - email: String @external - reviews: [Review] -} -`); - }); - it('keeps custom directives', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - - const schema = buildFederatedSchema(gql` - directive @custom on FIELD - - extend type User @key(fields: "email") { - email: String @external - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl).toEqual(`directive @custom on FIELD - -extend type User @key(fields: "email") { - email: String @external -} -`); - }); - }); -}); - -describe('legacy interface', () => { - const resolvers = { - Query: { - product: () => ({}), - }, - Product: { - upc: () => '1234', - price: () => 10, - }, - }; - const typeDefs: DocumentNode[] = [ - gql` - type Query { - product: Product - } - type Product @key(fields: "upc") { - upc: String! - name: String - } - `, - gql` - extend type Product { - price: Int - } - `, - ]; - it('allows legacy schema module interface as an input with an array of typeDefs and resolvers', async () => { - const schema = buildFederatedSchema({ typeDefs, resolvers }); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - expect( - await execute( - schema, - gql` - { - product { - price - upc - } - } - `, - ), - ).toEqual({ - data: { - product: { upc: '1234', price: 10 }, - }, - }); - }); - it('allows legacy schema module interface as a single module', async () => { - const schema = buildFederatedSchema({ - typeDefs: gql` - type Query { - product: Product - } - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `, - resolvers, - }); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - expect( - await execute( - schema, - gql` - { - product { - price - upc - } - } - `, - ), - ).toEqual({ - data: { - product: { upc: '1234', price: 10 }, - }, - }); - }); - it('allows legacy schema module interface as a single module without resolvers', async () => { - const schema = buildFederatedSchema({ - typeDefs: gql` - type Query { - product: Product - } - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `, - }); - expect(schema.getType('Product')).toMatchInlineSnapshot(` -type Product { - upc: String! - name: String - price: Int -} -`); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - }); - it('allows legacy schema module interface as a simple array of documents', async () => { - const schema = buildFederatedSchema({ typeDefs }); - expect(schema.getType('Product')).toMatchInlineSnapshot(` -type Product { - upc: String! - name: String - price: Int -} -`); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - }); -}); diff --git a/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts b/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts deleted file mode 100644 index 787210e25a2..00000000000 --- a/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { fixtures } from 'apollo-federation-integration-testsuite'; -import { composeAndValidate } from '../../composition'; -import { parse, print, GraphQLError, visit, StringValueNode } from 'graphql'; - -describe('printComposedSdl', () => { - let composedSdl: string, errors: GraphQLError[]; - - beforeAll(() => { - // composeAndValidate calls `printComposedSdl` to return `composedSdl` - ({ composedSdl, errors } = composeAndValidate(fixtures)); - }); - - it('composes without errors', () => { - expect(errors).toHaveLength(0); - }); - - it('produces a parseable output', () => { - expect(() => parse(composedSdl)).not.toThrow(); - }) - - it('prints a fully composed schema correctly', () => { - expect(composedSdl).toMatchInlineSnapshot(` - "schema - @graph(name: \\"accounts\\", url: \\"https://accounts.api.com\\") - @graph(name: \\"books\\", url: \\"https://books.api.com\\") - @graph(name: \\"documents\\", url: \\"https://documents.api.com\\") - @graph(name: \\"inventory\\", url: \\"https://inventory.api.com\\") - @graph(name: \\"product\\", url: \\"https://product.api.com\\") - @graph(name: \\"reviews\\", url: \\"https://reviews.api.com\\") - @composedGraph(version: 1) - { - query: Query - mutation: Mutation - } - - directive @composedGraph(version: Int!) on SCHEMA - - directive @graph(name: String!, url: String!) on SCHEMA - - directive @owner(graph: String!) on OBJECT - - directive @key(fields: String!, graph: String!) on OBJECT - - directive @resolve(graph: String!) on FIELD_DEFINITION - - directive @provides(fields: String!) on FIELD_DEFINITION - - directive @requires(fields: String!) on FIELD_DEFINITION - - directive @stream on FIELD - - directive @transform(from: String!) on FIELD - - union AccountType = PasswordAccount | SMSAccount - - type Amazon { - referrer: String - } - - union Body = Image | Text - - type Book implements Product - @owner(graph: \\"books\\") - @key(fields: \\"{ isbn }\\", graph: \\"books\\") - @key(fields: \\"{ isbn }\\", graph: \\"inventory\\") - @key(fields: \\"{ isbn }\\", graph: \\"product\\") - @key(fields: \\"{ isbn }\\", graph: \\"reviews\\") - { - isbn: String! - title: String - year: Int - similarBooks: [Book]! - metadata: [MetadataOrError] - inStock: Boolean @resolve(graph: \\"inventory\\") - isCheckedOut: Boolean @resolve(graph: \\"inventory\\") - upc: String! @resolve(graph: \\"product\\") - sku: String! @resolve(graph: \\"product\\") - name(delimeter: String = \\" \\"): String @resolve(graph: \\"product\\") @requires(fields: \\"{ title year }\\") - price: String @resolve(graph: \\"product\\") - details: ProductDetailsBook @resolve(graph: \\"product\\") - reviews: [Review] @resolve(graph: \\"reviews\\") - relatedReviews: [Review!]! @resolve(graph: \\"reviews\\") @requires(fields: \\"{ similarBooks { isbn } }\\") - } - - union Brand = Ikea | Amazon - - type Car implements Vehicle - @owner(graph: \\"product\\") - @key(fields: \\"{ id }\\", graph: \\"product\\") - @key(fields: \\"{ id }\\", graph: \\"reviews\\") - { - id: String! - description: String - price: String - retailPrice: String @resolve(graph: \\"reviews\\") @requires(fields: \\"{ price }\\") - } - - type Error { - code: Int - message: String - } - - type Furniture implements Product - @owner(graph: \\"product\\") - @key(fields: \\"{ upc }\\", graph: \\"product\\") - @key(fields: \\"{ sku }\\", graph: \\"product\\") - @key(fields: \\"{ sku }\\", graph: \\"inventory\\") - @key(fields: \\"{ upc }\\", graph: \\"reviews\\") - { - upc: String! - sku: String! - name: String - price: String - brand: Brand - metadata: [MetadataOrError] - details: ProductDetailsFurniture - inStock: Boolean @resolve(graph: \\"inventory\\") - isHeavy: Boolean @resolve(graph: \\"inventory\\") - reviews: [Review] @resolve(graph: \\"reviews\\") - } - - type Ikea { - asile: Int - } - - type Image { - name: String! - attributes: ImageAttributes! - } - - type ImageAttributes { - url: String! - } - - type KeyValue { - key: String! - value: String! - } - - type Library - @owner(graph: \\"books\\") - @key(fields: \\"{ id }\\", graph: \\"books\\") - @key(fields: \\"{ id }\\", graph: \\"accounts\\") - { - id: ID! - name: String - userAccount(id: ID! = 1): User @resolve(graph: \\"accounts\\") @requires(fields: \\"{ name }\\") - } - - union MetadataOrError = KeyValue | Error - - type Mutation { - login(username: String!, password: String!): User @resolve(graph: \\"accounts\\") - reviewProduct(upc: String!, body: String!): Product @resolve(graph: \\"reviews\\") - updateReview(review: UpdateReviewInput!): Review @resolve(graph: \\"reviews\\") - deleteReview(id: ID!): Boolean @resolve(graph: \\"reviews\\") - } - - type Name { - first: String - last: String - } - - type PasswordAccount - @owner(graph: \\"accounts\\") - @key(fields: \\"{ email }\\", graph: \\"accounts\\") - { - email: String! - } - - interface Product { - upc: String! - sku: String! - name: String - price: String - details: ProductDetails - inStock: Boolean - reviews: [Review] - } - - interface ProductDetails { - country: String - } - - type ProductDetailsBook implements ProductDetails { - country: String - pages: Int - } - - type ProductDetailsFurniture implements ProductDetails { - country: String - color: String - } - - type Query { - user(id: ID!): User @resolve(graph: \\"accounts\\") - me: User @resolve(graph: \\"accounts\\") - book(isbn: String!): Book @resolve(graph: \\"books\\") - books: [Book] @resolve(graph: \\"books\\") - library(id: ID!): Library @resolve(graph: \\"books\\") - body: Body! @resolve(graph: \\"documents\\") - product(upc: String!): Product @resolve(graph: \\"product\\") - vehicle(id: String!): Vehicle @resolve(graph: \\"product\\") - topProducts(first: Int = 5): [Product] @resolve(graph: \\"product\\") - topCars(first: Int = 5): [Car] @resolve(graph: \\"product\\") - topReviews(first: Int = 5): [Review] @resolve(graph: \\"reviews\\") - } - - type Review - @owner(graph: \\"reviews\\") - @key(fields: \\"{ id }\\", graph: \\"reviews\\") - { - id: ID! - body(format: Boolean = false): String - author: User @provides(fields: \\"{ username }\\") - product: Product - metadata: [MetadataOrError] - } - - type SMSAccount - @owner(graph: \\"accounts\\") - @key(fields: \\"{ number }\\", graph: \\"accounts\\") - { - number: String - } - - type Text { - name: String! - attributes: TextAttributes! - } - - type TextAttributes { - bold: Boolean - text: String - } - - union Thing = Car | Ikea - - input UpdateReviewInput { - id: ID! - body: String - } - - type User - @owner(graph: \\"accounts\\") - @key(fields: \\"{ id }\\", graph: \\"accounts\\") - @key(fields: \\"{ username name { first last } }\\", graph: \\"accounts\\") - @key(fields: \\"{ id }\\", graph: \\"inventory\\") - @key(fields: \\"{ id }\\", graph: \\"product\\") - @key(fields: \\"{ id }\\", graph: \\"reviews\\") - { - id: ID! - name: Name - username: String - birthDate(locale: String): String - account: AccountType - metadata: [UserMetadata] - goodDescription: Boolean @resolve(graph: \\"inventory\\") @requires(fields: \\"{ metadata { description } }\\") - vehicle: Vehicle @resolve(graph: \\"product\\") - thing: Thing @resolve(graph: \\"product\\") - reviews: [Review] @resolve(graph: \\"reviews\\") - numberOfReviews: Int! @resolve(graph: \\"reviews\\") - goodAddress: Boolean @resolve(graph: \\"reviews\\") @requires(fields: \\"{ metadata { address } }\\") - } - - type UserMetadata { - name: String - address: String - description: String - } - - type Van implements Vehicle - @owner(graph: \\"product\\") - @key(fields: \\"{ id }\\", graph: \\"product\\") - @key(fields: \\"{ id }\\", graph: \\"reviews\\") - { - id: String! - description: String - price: String - retailPrice: String @resolve(graph: \\"reviews\\") @requires(fields: \\"{ price }\\") - } - - interface Vehicle { - id: String! - description: String - price: String - retailPrice: String - } - " - `); - }); - - it('fieldsets are parseable', () => { - const parsedCsdl = parse(composedSdl); - const fieldSets: string[] = []; - - // Collect all args with the 'fields' name (from @key, @provides, @requires directives) - visit(parsedCsdl, { - Argument(node) { - if (node.name.value === 'fields') { - fieldSets.push((node.value as StringValueNode).value); - } - }, - }); - - // Ensure each found 'fields' arg is graphql parseable - fieldSets.forEach((unparsed) => { - expect(() => parse(unparsed)).not.toThrow(); - }); - }); -}); diff --git a/packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts deleted file mode 100644 index b0eddadb79b..00000000000 --- a/packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { fixtures } from 'apollo-federation-integration-testsuite'; -import { composeAndValidate } from '../../composition'; -import { printSchema } from '../printFederatedSchema'; - -describe('printFederatedSchema', () => { - const { schema, errors } = composeAndValidate(fixtures); - - it('composes without errors', () => { - expect(errors).toHaveLength(0); - }); - - it('prints a fully composed schema correctly', () => { - expect(printSchema(schema)).toMatchInlineSnapshot(` - "directive @stream on FIELD - - directive @transform(from: String!) on FIELD - - union AccountType = PasswordAccount | SMSAccount - - type Amazon { - referrer: String - } - - union Body = Image | Text - - type Book implements Product @key(fields: \\"isbn\\") { - isbn: String! - title: String - year: Int - similarBooks: [Book]! - metadata: [MetadataOrError] - inStock: Boolean - isCheckedOut: Boolean - upc: String! - sku: String! - name(delimeter: String = \\" \\"): String @requires(fields: \\"title year\\") - price: String - details: ProductDetailsBook - reviews: [Review] - relatedReviews: [Review!]! @requires(fields: \\"similarBooks { isbn }\\") - } - - union Brand = Ikea | Amazon - - type Car implements Vehicle @key(fields: \\"id\\") { - id: String! - description: String - price: String - retailPrice: String @requires(fields: \\"price\\") - } - - type Error { - code: Int - message: String - } - - type Furniture implements Product @key(fields: \\"sku\\") @key(fields: \\"upc\\") { - upc: String! - sku: String! - name: String - price: String - brand: Brand - metadata: [MetadataOrError] - details: ProductDetailsFurniture - inStock: Boolean - isHeavy: Boolean - reviews: [Review] - } - - type Ikea { - asile: Int - } - - type Image { - name: String! - attributes: ImageAttributes! - } - - type ImageAttributes { - url: String! - } - - type KeyValue { - key: String! - value: String! - } - - type Library @key(fields: \\"id\\") { - id: ID! - name: String - userAccount(id: ID! = 1): User @requires(fields: \\"name\\") - } - - union MetadataOrError = KeyValue | Error - - type Mutation { - login(username: String!, password: String!): User - reviewProduct(upc: String!, body: String!): Product - updateReview(review: UpdateReviewInput!): Review - deleteReview(id: ID!): Boolean - } - - type Name { - first: String - last: String - } - - type PasswordAccount @key(fields: \\"email\\") { - email: String! - } - - interface Product { - upc: String! - sku: String! - name: String - price: String - details: ProductDetails - inStock: Boolean - reviews: [Review] - } - - interface ProductDetails { - country: String - } - - type ProductDetailsBook implements ProductDetails { - country: String - pages: Int - } - - type ProductDetailsFurniture implements ProductDetails { - country: String - color: String - } - - type Query { - user(id: ID!): User - me: User - book(isbn: String!): Book - books: [Book] - library(id: ID!): Library - body: Body! - product(upc: String!): Product - vehicle(id: String!): Vehicle - topProducts(first: Int = 5): [Product] - topCars(first: Int = 5): [Car] - topReviews(first: Int = 5): [Review] - } - - type Review @key(fields: \\"id\\") { - id: ID! - body(format: Boolean = false): String - author: User @provides(fields: \\"username\\") - product: Product - metadata: [MetadataOrError] - } - - type SMSAccount @key(fields: \\"number\\") { - number: String - } - - type Text { - name: String! - attributes: TextAttributes! - } - - type TextAttributes { - bold: Boolean - text: String - } - - union Thing = Car | Ikea - - input UpdateReviewInput { - id: ID! - body: String - } - - type User @key(fields: \\"id\\") @key(fields: \\"username name { first last }\\") { - id: ID! - name: Name - username: String - birthDate(locale: String): String - account: AccountType - metadata: [UserMetadata] - goodDescription: Boolean @requires(fields: \\"metadata { description }\\") - vehicle: Vehicle - thing: Thing - reviews: [Review] - numberOfReviews: Int! - goodAddress: Boolean @requires(fields: \\"metadata { address }\\") - } - - type UserMetadata { - name: String - address: String - description: String - } - - type Van implements Vehicle @key(fields: \\"id\\") { - id: String! - description: String - price: String - retailPrice: String @requires(fields: \\"price\\") - } - - interface Vehicle { - id: String! - description: String - price: String - retailPrice: String - } - " - `); - }); -}); diff --git a/packages/apollo-federation/src/service/__tests__/tsconfig.json b/packages/apollo-federation/src/service/__tests__/tsconfig.json deleted file mode 100644 index bf17bf5a60c..00000000000 --- a/packages/apollo-federation/src/service/__tests__/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [{ "path": "../../../" }] -} diff --git a/packages/apollo-federation/src/service/buildFederatedSchema.ts b/packages/apollo-federation/src/service/buildFederatedSchema.ts deleted file mode 100644 index 16481e0902b..00000000000 --- a/packages/apollo-federation/src/service/buildFederatedSchema.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - DocumentNode, - GraphQLSchema, - isObjectType, - isUnionType, - GraphQLUnionType, - GraphQLObjectType, - specifiedDirectives, -} from 'graphql'; -import { - buildSchemaFromSDL, - transformSchema, - GraphQLSchemaModule, - modulesFromSDL, - addResolversToSchema, - GraphQLResolverMap, -} from 'apollo-graphql'; -import federationDirectives, { typeIncludesDirective } from '../directives'; - -import { serviceField, entitiesField, EntityType } from '../types'; - -import { printSchema } from './printFederatedSchema'; - -import 'apollo-server-env'; - -type LegacySchemaModule = { - typeDefs: DocumentNode | DocumentNode[]; - resolvers?: GraphQLResolverMap; -}; - -export function buildFederatedSchema( - modulesOrSDL: - | (GraphQLSchemaModule | DocumentNode)[] - | DocumentNode - | LegacySchemaModule, -): GraphQLSchema { - // ApolloServer supports passing an array of DocumentNode along with a single - // map of resolvers to build a schema. Long term we don't want to support this - // style anymore as we move towards a more structured approach to modules, - // however, it has tripped several teams up to not support this signature - // in buildFederatedSchema. Especially as teams migrate from - // `new ApolloServer({ typeDefs: DocumentNode[], resolvers })` to - // `new ApolloServer({ schema: buildFederatedSchema({ typeDefs: DocumentNode[], resolvers }) })` - // - // The last type in the union for `modulesOrSDL` supports this "legacy" input - // style in a simple manner (by just adding the resolvers to the first typeDefs entry) - // - let shapedModulesOrSDL: (GraphQLSchemaModule | DocumentNode)[] | DocumentNode; - if ('typeDefs' in modulesOrSDL) { - const { typeDefs, resolvers } = modulesOrSDL; - const augmentedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs]; - shapedModulesOrSDL = augmentedTypeDefs.map((typeDefs, i) => { - const module: GraphQLSchemaModule = { typeDefs }; - // add the resolvers to the first "module" in the array - if (i === 0 && resolvers) module.resolvers = resolvers; - return module; - }); - } else { - shapedModulesOrSDL = modulesOrSDL; - } - - const modules = modulesFromSDL(shapedModulesOrSDL); - - let schema = buildSchemaFromSDL( - modules, - new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }), - ); - - // At this point in time, we have a schema to be printed into SDL which is - // representative of what the user defined for their schema. This is before - // we process any of the federation directives and add custom federation types - // so its the right place to create our service definition sdl. - // - // We have to use a modified printSchema from graphql-js which includes - // support for preserving the *uses* of federation directives while removing - // their *definitions* from the sdl. - const sdl = printSchema(schema); - - // Add an empty query root type if none has been defined - if (!schema.getQueryType()) { - schema = new GraphQLSchema({ - ...schema.toConfig(), - query: new GraphQLObjectType({ - name: 'Query', - fields: {}, - }), - }); - } - - const entityTypes = Object.values(schema.getTypeMap()).filter( - type => isObjectType(type) && typeIncludesDirective(type, 'key'), - ); - const hasEntities = entityTypes.length > 0; - - schema = transformSchema(schema, type => { - // Add `_entities` and `_service` fields to query root type - if (isObjectType(type) && type === schema.getQueryType()) { - const config = type.toConfig(); - return new GraphQLObjectType({ - ...config, - fields: { - ...(hasEntities && { _entities: entitiesField }), - _service: { - ...serviceField, - resolve: () => ({ sdl }), - }, - ...config.fields, - }, - }); - } - - return undefined; - }); - - schema = transformSchema(schema, type => { - if (hasEntities && isUnionType(type) && type.name === EntityType.name) { - return new GraphQLUnionType({ - ...EntityType.toConfig(), - types: entityTypes.filter(isObjectType), - }); - } - return undefined; - }); - - for (const module of modules) { - if (!module.resolvers) continue; - addResolversToSchema(schema, module.resolvers); - } - - return schema; -} diff --git a/packages/apollo-federation/src/service/index.ts b/packages/apollo-federation/src/service/index.ts deleted file mode 100644 index c8cfc974da2..00000000000 --- a/packages/apollo-federation/src/service/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './buildFederatedSchema'; -export * from './printFederatedSchema'; diff --git a/packages/apollo-federation/src/service/printComposedSdl.ts b/packages/apollo-federation/src/service/printComposedSdl.ts deleted file mode 100644 index 3ed6f94c8b6..00000000000 --- a/packages/apollo-federation/src/service/printComposedSdl.ts +++ /dev/null @@ -1,514 +0,0 @@ -import { - GraphQLSchema, - isSpecifiedDirective, - isIntrospectionType, - isSpecifiedScalarType, - GraphQLNamedType, - GraphQLDirective, - isScalarType, - isObjectType, - isInterfaceType, - isUnionType, - isEnumType, - isInputObjectType, - GraphQLScalarType, - GraphQLObjectType, - GraphQLInterfaceType, - GraphQLUnionType, - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLArgument, - GraphQLInputField, - astFromValue, - print, - GraphQLField, - GraphQLEnumValue, - GraphQLString, - DEFAULT_DEPRECATION_REASON, - ASTNode, - SelectionNode, -} from 'graphql'; -import { Maybe, ServiceDefinition, FederationType, FederationField } from '../composition'; -import { isFederationType } from '../types'; -import { isFederationDirective } from '../composition/utils'; -import csdlDirectives from '../csdlDirectives'; - -type Options = { - /** - * Descriptions are defined as preceding string literals, however an older - * experimental version of the SDL supported preceding comments as - * descriptions. Set to true to enable this deprecated behavior. - * This option is provided to ease adoption and will be removed in v16. - * - * Default: false - */ - commentDescriptions?: boolean; -}; - -/** - * Accepts options as a second argument: - * - * - commentDescriptions: - * Provide true to use preceding comments as the description. - * - */ -export function printComposedSdl( - schema: GraphQLSchema, - serviceList: ServiceDefinition[], - options?: Options, -): string { - return printFilteredSchema( - schema, - // Federation change: we need service and url information for the @graph directives - serviceList, - // Federation change: treat the directives defined by the federation spec - // similarly to the directives defined by the GraphQL spec (ie, don't print - // their definitions). - (n) => !isSpecifiedDirective(n) && !isFederationDirective(n), - isDefinedType, - options, - ); -} - -export function printIntrospectionSchema( - schema: GraphQLSchema, - options?: Options, -): string { - return printFilteredSchema( - schema, - [], - isSpecifiedDirective, - isIntrospectionType, - options, - ); -} - -// Federation change: treat the types defined by the federation spec -// similarly to the directives defined by the GraphQL spec (ie, don't print -// their definitions). -function isDefinedType(type: GraphQLNamedType): boolean { - return ( - !isSpecifiedScalarType(type) && - !isIntrospectionType(type) && - !isFederationType(type) - ); -} - -function printFilteredSchema( - schema: GraphQLSchema, - // Federation change: we need service and url information for the @graph directives - serviceList: ServiceDefinition[], - directiveFilter: (type: GraphQLDirective) => boolean, - typeFilter: (type: GraphQLNamedType) => boolean, - options?: Options, -): string { - // Federation change: include directive definitions for CSDL - const directives = [ - ...csdlDirectives, - ...schema.getDirectives().filter(directiveFilter), - ]; - const types = Object.values(schema.getTypeMap()) - .sort((type1, type2) => type1.name.localeCompare(type2.name)) - .filter(typeFilter); - - return ( - [printSchemaDefinition(schema, serviceList)] - .concat( - directives.map(directive => printDirective(directive, options)), - types.map(type => printType(type, options)), - ) - .filter(Boolean) - .join('\n\n') + '\n' - ); -} - -function printSchemaDefinition( - schema: GraphQLSchema, - serviceList: ServiceDefinition[], -): string | undefined { - const operationTypes = []; - - const queryType = schema.getQueryType(); - if (queryType) { - operationTypes.push(` query: ${queryType.name}`); - } - - const mutationType = schema.getMutationType(); - if (mutationType) { - operationTypes.push(` mutation: ${mutationType.name}`); - } - - const subscriptionType = schema.getSubscriptionType(); - if (subscriptionType) { - operationTypes.push(` subscription: ${subscriptionType.name}`); - } - - return ( - 'schema' + - // Federation change: print @graph and @composedGraph schema directives - printFederationSchemaDirectives(serviceList) + - `\n{\n${operationTypes.join('\n')}\n}` - ); -} - -function printFederationSchemaDirectives(serviceList: ServiceDefinition[]) { - return ( - serviceList.map(service => `\n @graph(name: "${service.name}", url: "${service.url}")`).join('') + - `\n @composedGraph(version: 1)` - ); -} - -export function printType(type: GraphQLNamedType, options?: Options): string { - if (isScalarType(type)) { - return printScalar(type, options); - } else if (isObjectType(type)) { - return printObject(type, options); - } else if (isInterfaceType(type)) { - return printInterface(type, options); - } else if (isUnionType(type)) { - return printUnion(type, options); - } else if (isEnumType(type)) { - return printEnum(type, options); - } else if (isInputObjectType(type)) { - return printInputObject(type, options); - } - - throw Error('Unexpected type: ' + (type as GraphQLNamedType).toString()); -} - -function printScalar(type: GraphQLScalarType, options?: Options): string { - return printDescription(options, type) + `scalar ${type.name}`; -} - -function printObject(type: GraphQLObjectType, options?: Options): string { - const interfaces = type.getInterfaces(); - const implementedInterfaces = interfaces.length - ? ' implements ' + interfaces.map(i => i.name).join(' & ') - : ''; - - // Federation change: print `extend` keyword on type extensions. - // - // The implementation assumes that an owned type will have fields defined - // since that is required for a valid schema. Types that are *only* - // extensions will not have fields on the astNode since that ast doesn't - // exist. - // - // XXX revist extension checking - const isExtension = - type.extensionASTNodes && type.astNode && !type.astNode.fields; - - return ( - printDescription(options, type) + - (isExtension ? 'extend ' : '') + - `type ${type.name}` + - implementedInterfaces + - // Federation addition for printing @owner and @key usages - printFederationTypeDirectives(type) + - printFields(options, type) - ); -} - -// Federation change: print usages of the @owner and @key directives. -function printFederationTypeDirectives(type: GraphQLObjectType): string { - const metadata: FederationType = type.extensions?.federation; - if (!metadata) return ''; - - const { serviceName: ownerService, keys } = metadata; - if (!ownerService || !keys) return ''; - - // Separate owner @keys from the rest of the @keys so we can print them - // adjacent to the @owner directive. - const { [ownerService]: ownerKeys, ...restKeys } = keys - const ownerEntry: [string, (readonly SelectionNode[])[]] = [ownerService, ownerKeys]; - const restEntries = Object.entries(restKeys); - - return ( - `\n @owner(graph: "${ownerService}")` + - [ownerEntry, ...restEntries].map(([service, keys]) => - keys - .map( - (selections) => - `\n @key(fields: "${printFieldSet(selections)}", graph: "${service}")`, - ) - .join(''), - ) - .join('') - ); -} - -function printInterface(type: GraphQLInterfaceType, options?: Options): string { - // Federation change: print `extend` keyword on type extensions. - // See printObject for assumptions made. - // - // XXX revist extension checking - const isExtension = - type.extensionASTNodes && type.astNode && !type.astNode.fields; - - return ( - printDescription(options, type) + - (isExtension ? 'extend ' : '') + - `interface ${type.name}` + - printFields(options, type) - ); -} - -function printUnion(type: GraphQLUnionType, options?: Options): string { - const types = type.getTypes(); - const possibleTypes = types.length ? ' = ' + types.join(' | ') : ''; - return printDescription(options, type) + 'union ' + type.name + possibleTypes; -} - -function printEnum(type: GraphQLEnumType, options?: Options): string { - const values = type - .getValues() - .map( - (value, i) => - printDescription(options, value, ' ', !i) + - ' ' + - value.name + - printDeprecated(value), - ); - - return ( - printDescription(options, type) + `enum ${type.name}` + printBlock(values) - ); -} - -function printInputObject( - type: GraphQLInputObjectType, - options?: Options, -): string { - const fields = Object.values(type.getFields()).map( - (f, i) => - printDescription(options, f, ' ', !i) + ' ' + printInputValue(f), - ); - return ( - printDescription(options, type) + `input ${type.name}` + printBlock(fields) - ); -} - -function printFields( - options: Options | undefined, - type: GraphQLObjectType | GraphQLInterfaceType, -) { - - const fields = Object.values(type.getFields()).map( - (f, i) => - printDescription(options, f, ' ', !i) + - ' ' + - f.name + - printArgs(options, f.args, ' ') + - ': ' + - String(f.type) + - printDeprecated(f) + - printFederationFieldDirectives(f, type), - ); - - // Federation change: for entities, we want to print the block on a new line. - // This is just a formatting nice-to-have. - const isEntity = Boolean(type.extensions?.federation?.keys); - - return printBlock(fields, isEntity); -} - -export function printWithReducedWhitespace(ast: ASTNode): string { - return print(ast) - .replace(/\s+/g, ' ') - .trim(); -} - -/** - * Federation change: print fieldsets for @key, @requires, and @provides directives - * - * @param selections - */ -function printFieldSet(selections: readonly SelectionNode[]): string { - return `{ ${selections.map(printWithReducedWhitespace).join(' ')} }`; -} - -/** - * Federation change: print @resolve, @requires, and @provides directives - * - * @param field - * @param parentType - */ -function printFederationFieldDirectives( - field: GraphQLField, - parentType: GraphQLObjectType | GraphQLInterfaceType, -): string { - if (!field.extensions?.federation) return ''; - - const { - serviceName, - requires = [], - provides = [], - }: FederationField = field.extensions.federation; - - let printed = ''; - // If a `serviceName` exists, we only want to print a `@resolve` directive - // if the `serviceName` differs from the `parentType`'s `serviceName` - if ( - serviceName && - serviceName !== parentType.extensions?.federation?.serviceName - ) { - printed += ` @resolve(graph: "${serviceName}")`; - } - - if (requires.length > 0) { - printed += ` @requires(fields: "${printFieldSet(requires)}")`; - } - - if (provides.length > 0) { - printed += ` @provides(fields: "${printFieldSet(provides)}")`; - } - - return printed; -} - -// Federation change: `onNewLine` is a formatting nice-to-have for printing -// types that have a list of directives attached, i.e. an entity. -function printBlock(items: string[], onNewLine?: boolean) { - return items.length !== 0 - ? onNewLine - ? '\n{\n' + items.join('\n') + '\n}' - : ' {\n' + items.join('\n') + '\n}' - : ''; -} - -function printArgs( - options: Options | undefined, - args: GraphQLArgument[], - indentation = '', -) { - if (args.length === 0) { - return ''; - } - - // If every arg does not have a description, print them on one line. - if (args.every((arg) => !arg.description)) { - return '(' + args.map(printInputValue).join(', ') + ')'; - } - - return ( - '(\n' + - args - .map( - (arg, i) => - printDescription(options, arg, ' ' + indentation, !i) + - ' ' + - indentation + - printInputValue(arg), - ) - .join('\n') + - '\n' + - indentation + - ')' - ); -} - -function printInputValue(arg: GraphQLInputField) { - const defaultAST = astFromValue(arg.defaultValue, arg.type); - let argDecl = arg.name + ': ' + String(arg.type); - if (defaultAST) { - argDecl += ` = ${print(defaultAST)}`; - } - return argDecl; -} - -function printDirective(directive: GraphQLDirective, options?: Options) { - return ( - printDescription(options, directive) + - 'directive @' + - directive.name + - printArgs(options, directive.args) + - (directive.isRepeatable ? ' repeatable' : '') + - ' on ' + - directive.locations.join(' | ') - ); -} - -function printDeprecated( - fieldOrEnumVal: GraphQLField | GraphQLEnumValue, -) { - if (!fieldOrEnumVal.isDeprecated) { - return ''; - } - const reason = fieldOrEnumVal.deprecationReason; - const reasonAST = astFromValue(reason, GraphQLString); - if (reasonAST && reason !== DEFAULT_DEPRECATION_REASON) { - return ' @deprecated(reason: ' + print(reasonAST) + ')'; - } - return ' @deprecated'; -} - -function printDescription }>( - options: Options | undefined, - def: T, - indentation = '', - firstInBlock = true, -): string { - const { description } = def; - if (description == null) { - return ''; - } - - if (options?.commentDescriptions === true) { - return printDescriptionWithComments(description, indentation, firstInBlock); - } - - const preferMultipleLines = description.length > 70; - const blockString = printBlockString(description, '', preferMultipleLines); - const prefix = - indentation && !firstInBlock ? '\n' + indentation : indentation; - - return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n'; -} - -function printDescriptionWithComments( - description: string, - indentation: string, - firstInBlock: boolean, -) { - const prefix = indentation && !firstInBlock ? '\n' : ''; - const comment = description - .split('\n') - .map((line) => indentation + (line !== '' ? '# ' + line : '#')) - .join('\n'); - - return prefix + comment + '\n'; -} - -/** - * Print a block string in the indented block form by adding a leading and - * trailing blank line. However, if a block string starts with whitespace and is - * a single-line, adding a leading blank line would strip that whitespace. - * - * @internal - */ -export function printBlockString( - value: string, - indentation: string = '', - preferMultipleLines: boolean = false, -): string { - const isSingleLine = value.indexOf('\n') === -1; - const hasLeadingSpace = value[0] === ' ' || value[0] === '\t'; - const hasTrailingQuote = value[value.length - 1] === '"'; - const hasTrailingSlash = value[value.length - 1] === '\\'; - const printAsMultipleLines = - !isSingleLine || - hasTrailingQuote || - hasTrailingSlash || - preferMultipleLines; - - let result = ''; - // Format a multi-line block quote to account for leading space. - if (printAsMultipleLines && !(isSingleLine && hasLeadingSpace)) { - result += '\n' + indentation; - } - result += indentation ? value.replace(/\n/g, '\n' + indentation) : value; - if (printAsMultipleLines) { - result += '\n'; - } - - return '"""' + result.replace(/"""/g, '\\"""') + '"""'; -} diff --git a/packages/apollo-federation/src/service/printFederatedSchema.ts b/packages/apollo-federation/src/service/printFederatedSchema.ts deleted file mode 100644 index 0d963d6c0b7..00000000000 --- a/packages/apollo-federation/src/service/printFederatedSchema.ts +++ /dev/null @@ -1,454 +0,0 @@ -/** - * Forked from graphql-js schemaPrinter.js file @ v14.7.0 - * This file has been modified to support printing federated - * schema, including associated federation directives. - */ - -import { - GraphQLSchema, - isSpecifiedDirective, - isIntrospectionType, - isSpecifiedScalarType, - GraphQLNamedType, - GraphQLDirective, - isScalarType, - isObjectType, - isInterfaceType, - isUnionType, - isEnumType, - isInputObjectType, - GraphQLScalarType, - GraphQLObjectType, - GraphQLInterfaceType, - GraphQLUnionType, - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLArgument, - GraphQLInputField, - astFromValue, - print, - GraphQLField, - GraphQLEnumValue, - GraphQLString, - DEFAULT_DEPRECATION_REASON, - ASTNode, -} from 'graphql'; -import { Maybe } from '../composition'; -import { isFederationType } from '../types'; -import { isFederationDirective } from '../composition/utils'; -import federationDirectives, { gatherDirectives } from '../directives'; - -type Options = { - /** - * Descriptions are defined as preceding string literals, however an older - * experimental version of the SDL supported preceding comments as - * descriptions. Set to true to enable this deprecated behavior. - * This option is provided to ease adoption and will be removed in v16. - * - * Default: false - */ - commentDescriptions?: boolean; -}; - -/** - * Accepts options as a second argument: - * - * - commentDescriptions: - * Provide true to use preceding comments as the description. - * - */ -export function printSchema(schema: GraphQLSchema, options?: Options): string { - return printFilteredSchema( - schema, - // Federation change: treat the directives defined by the federation spec - // similarly to the directives defined by the GraphQL spec (ie, don't print - // their definitions). - (n) => !isSpecifiedDirective(n) && !isFederationDirective(n), - isDefinedType, - options, - ); -} - -export function printIntrospectionSchema( - schema: GraphQLSchema, - options?: Options, -): string { - return printFilteredSchema( - schema, - isSpecifiedDirective, - isIntrospectionType, - options, - ); -} - -// Federation change: treat the types defined by the federation spec -// similarly to the directives defined by the GraphQL spec (ie, don't print -// their definitions). -function isDefinedType(type: GraphQLNamedType): boolean { - return ( - !isSpecifiedScalarType(type) && - !isIntrospectionType(type) && - !isFederationType(type) - ); -} - -function printFilteredSchema( - schema: GraphQLSchema, - directiveFilter: (type: GraphQLDirective) => boolean, - typeFilter: (type: GraphQLNamedType) => boolean, - options?: Options, -): string { - const directives = schema.getDirectives().filter(directiveFilter); - const types = Object.values(schema.getTypeMap()) - .sort((type1, type2) => type1.name.localeCompare(type2.name)) - .filter(typeFilter); - - return ( - [printSchemaDefinition(schema)] - .concat( - directives.map(directive => printDirective(directive, options)), - types.map(type => printType(type, options)), - ) - .filter(Boolean) - .join('\n\n') + '\n' - ); -} - -function printSchemaDefinition(schema: GraphQLSchema): string | undefined { - if (isSchemaOfCommonNames(schema)) { - return; - } - - const operationTypes = []; - - const queryType = schema.getQueryType(); - if (queryType) { - operationTypes.push(` query: ${queryType.name}`); - } - - const mutationType = schema.getMutationType(); - if (mutationType) { - operationTypes.push(` mutation: ${mutationType.name}`); - } - - const subscriptionType = schema.getSubscriptionType(); - if (subscriptionType) { - operationTypes.push(` subscription: ${subscriptionType.name}`); - } - - return `schema {\n${operationTypes.join('\n')}\n}`; -} - -/** - * GraphQL schema define root types for each type of operation. These types are - * the same as any other type and can be named in any manner, however there is - * a common naming convention: - * - * schema { - * query: Query - * mutation: Mutation - * } - * - * When using this naming convention, the schema description can be omitted. - */ -function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { - const queryType = schema.getQueryType(); - if (queryType && queryType.name !== 'Query') { - return false; - } - - const mutationType = schema.getMutationType(); - if (mutationType && mutationType.name !== 'Mutation') { - return false; - } - - const subscriptionType = schema.getSubscriptionType(); - if (subscriptionType && subscriptionType.name !== 'Subscription') { - return false; - } - - return true; -} - -export function printType(type: GraphQLNamedType, options?: Options): string { - if (isScalarType(type)) { - return printScalar(type, options); - } else if (isObjectType(type)) { - return printObject(type, options); - } else if (isInterfaceType(type)) { - return printInterface(type, options); - } else if (isUnionType(type)) { - return printUnion(type, options); - } else if (isEnumType(type)) { - return printEnum(type, options); - } else if (isInputObjectType(type)) { - return printInputObject(type, options); - } - - throw Error('Unexpected type: ' + (type as GraphQLNamedType).toString()); -} - -function printScalar(type: GraphQLScalarType, options?: Options): string { - return printDescription(options, type) + `scalar ${type.name}`; -} - -function printObject(type: GraphQLObjectType, options?: Options): string { - const interfaces = type.getInterfaces(); - const implementedInterfaces = interfaces.length - ? ' implements ' + interfaces.map(i => i.name).join(' & ') - : ''; - - // Federation change: print `extend` keyword on type extensions. - // - // The implementation assumes that an owned type will have fields defined - // since that is required for a valid schema. Types that are *only* - // extensions will not have fields on the astNode since that ast doesn't - // exist. - // - // XXX revist extension checking - const isExtension = - type.extensionASTNodes && type.astNode && !type.astNode.fields; - - return ( - printDescription(options, type) + - (isExtension ? 'extend ' : '') + - `type ${type.name}${implementedInterfaces}` + - // Federation addition for printing @key usages - printFederationDirectives(type) + - printFields(options, type) - ); -} - -function printInterface(type: GraphQLInterfaceType, options?: Options): string { - // Federation change: print `extend` keyword on type extensions. - // See printObject for assumptions made. - // - // XXX revist extension checking - const isExtension = - type.extensionASTNodes && type.astNode && !type.astNode.fields; - - return ( - printDescription(options, type) + - (isExtension ? 'extend ' : '') + - `interface ${type.name}` + - // Federation change: graphql@14 doesn't support interfaces implementing interfaces - // printImplementedInterfaces(type) + - printFederationDirectives(type) + - printFields(options, type) - ); -} - -function printUnion(type: GraphQLUnionType, options?: Options): string { - const types = type.getTypes(); - const possibleTypes = types.length ? ' = ' + types.join(' | ') : ''; - return printDescription(options, type) + 'union ' + type.name + possibleTypes; -} - -function printEnum(type: GraphQLEnumType, options?: Options): string { - const values = type - .getValues() - .map( - (value, i) => - printDescription(options, value, ' ', !i) + - ' ' + - value.name + - printDeprecated(value), - ); - - return ( - printDescription(options, type) + `enum ${type.name}` + printBlock(values) - ); -} - -function printInputObject(type: GraphQLInputObjectType, options?: Options): string { - const fields = Object.values(type.getFields()).map( - (f, i) => - printDescription(options, f, ' ', !i) + ' ' + printInputValue(f), - ); - return ( - printDescription(options, type) + `input ${type.name}` + printBlock(fields) - ); -} - -function printFields( - options: Options | undefined, - type: GraphQLObjectType | GraphQLInterfaceType, -) { - const fields = Object.values(type.getFields()).map( - (f, i) => - printDescription(options, f, ' ', !i) + - ' ' + - f.name + - printArgs(options, f.args, ' ') + - ': ' + - String(f.type) + - printDeprecated(f) + - printFederationDirectives(f), - ); - return printBlock(fields); -} - -// Federation change: *do* print the usages of federation directives. -function printFederationDirectives( - type: GraphQLNamedType | GraphQLField, -): string { - if (!type.astNode) return ''; - if (isInputObjectType(type)) return ''; - - const allDirectives = gatherDirectives(type) - .filter((n) => - federationDirectives.some((fedDir) => fedDir.name === n.name.value), - ) - .map(print); - const dedupedDirectives = [...new Set(allDirectives)]; - - return dedupedDirectives.length > 0 ? ' ' + dedupedDirectives.join(' ') : ''; -} - -export function printWithReducedWhitespace(ast: ASTNode): string { - return print(ast) - .replace(/\s+/g, ' ') - .trim(); -} - -function printBlock(items: string[]) { - return items.length !== 0 ? ' {\n' + items.join('\n') + '\n}' : ''; -} - -function printArgs( - options: Options | undefined, - args: GraphQLArgument[], - indentation = '', -) { - if (args.length === 0) { - return ''; - } - - // If every arg does not have a description, print them on one line. - if (args.every(arg => !arg.description)) { - return '(' + args.map(printInputValue).join(', ') + ')'; - } - - return ( - '(\n' + - args - .map( - (arg, i) => - printDescription(options, arg, ' ' + indentation, !i) + - ' ' + - indentation + - printInputValue(arg), - ) - .join('\n') + - '\n' + - indentation + - ')' - ); -} - -function printInputValue(arg: GraphQLInputField) { - const defaultAST = astFromValue(arg.defaultValue, arg.type); - let argDecl = arg.name + ': ' + String(arg.type); - if (defaultAST) { - argDecl += ` = ${print(defaultAST)}`; - } - return argDecl; -} - -function printDirective(directive: GraphQLDirective, options?: Options) { - return ( - printDescription(options, directive) + - 'directive @' + - directive.name + - printArgs(options, directive.args) + - (directive.isRepeatable ? ' repeatable' : '') + - ' on ' + - directive.locations.join(' | ') - ); -} - -function printDeprecated( - fieldOrEnumVal: GraphQLField | GraphQLEnumValue, -) { - if (!fieldOrEnumVal.isDeprecated) { - return ''; - } - const reason = fieldOrEnumVal.deprecationReason; - const reasonAST = astFromValue(reason, GraphQLString); - if (reasonAST && reason !== '' && reason !== DEFAULT_DEPRECATION_REASON) { - return ' @deprecated(reason: ' + print(reasonAST) + ')'; - } - return ' @deprecated'; -} - -function printDescription }>( - options: Options | undefined, - def: T, - indentation = '', - firstInBlock = true, -): string { - const { description } = def; - if (description == null) { - return ''; - } - - if (options?.commentDescriptions === true) { - return printDescriptionWithComments(description, indentation, firstInBlock); - } - - const preferMultipleLines = description.length > 70; - const blockString = printBlockString(description, '', preferMultipleLines); - const prefix = - indentation && !firstInBlock ? '\n' + indentation : indentation; - - return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n'; -} - -function printDescriptionWithComments( - description: string, - indentation: string, - firstInBlock: boolean, -) { - const prefix = indentation && !firstInBlock ? '\n' : ''; - const comment = description - .split('\n') - .map(line => indentation + (line !== '' ? '# ' + line : '#')) - .join('\n'); - - return prefix + comment + '\n'; -} - -/** - * Print a block string in the indented block form by adding a leading and - * trailing blank line. However, if a block string starts with whitespace and is - * a single-line, adding a leading blank line would strip that whitespace. - * - * @internal - */ -export function printBlockString( - value: string, - indentation: string = '', - preferMultipleLines: boolean = false, -): string { - const isSingleLine = value.indexOf('\n') === -1; - const hasLeadingSpace = value[0] === ' ' || value[0] === '\t'; - const hasTrailingQuote = value[value.length - 1] === '"'; - const hasTrailingSlash = value[value.length - 1] === '\\'; - const printAsMultipleLines = - !isSingleLine || - hasTrailingQuote || - hasTrailingSlash || - preferMultipleLines; - - let result = ''; - // Format a multi-line block quote to account for leading space. - if (printAsMultipleLines && !(isSingleLine && hasLeadingSpace)) { - result += '\n' + indentation; - } - result += indentation ? value.replace(/\n/g, '\n' + indentation) : value; - if (printAsMultipleLines) { - result += '\n'; - } - - return '"""' + result.replace(/"""/g, '\\"""') + '"""'; -} diff --git a/packages/apollo-federation/src/snapshotSerializers/astSerializer.ts b/packages/apollo-federation/src/snapshotSerializers/astSerializer.ts deleted file mode 100644 index b3dc7eb543b..00000000000 --- a/packages/apollo-federation/src/snapshotSerializers/astSerializer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ASTNode, print } from 'graphql'; -import { Plugin, Config, Refs } from 'pretty-format'; - -export default { - test(value: any) { - return value && typeof value.kind === 'string'; - }, - - serialize( - value: ASTNode, - _config: Config, - indentation: string, - _depth: number, - _refs: Refs, - _printer: any, - ): string { - return print(value) - .trim() - .replace(/\n/g, '\n' + indentation); - }, -} as Plugin; diff --git a/packages/apollo-federation/src/snapshotSerializers/graphqlErrorSerializer.ts b/packages/apollo-federation/src/snapshotSerializers/graphqlErrorSerializer.ts deleted file mode 100644 index 3363d747dbb..00000000000 --- a/packages/apollo-federation/src/snapshotSerializers/graphqlErrorSerializer.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { GraphQLError } from 'graphql'; -import { Plugin } from 'pretty-format'; - -export default { - test(value: any) { - return value && value instanceof GraphQLError; - }, - - print(value: GraphQLError, print) { - return print({ - message: value.message, - code: value.extensions ? value.extensions.code : 'MISSING_ERROR', - }); - }, -} as Plugin; diff --git a/packages/apollo-federation/src/snapshotSerializers/index.ts b/packages/apollo-federation/src/snapshotSerializers/index.ts deleted file mode 100644 index df46968bab7..00000000000 --- a/packages/apollo-federation/src/snapshotSerializers/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { default as astSerializer } from './astSerializer'; -export { default as selectionSetSerializer } from './selectionSetSerializer'; -export { default as typeSerializer } from './typeSerializer'; -export { default as graphqlErrorSerializer } from './graphqlErrorSerializer'; - -declare global { - namespace jest { - interface Expect { - /** - * Adds a module to format application-specific data structures for serialization. - */ - addSnapshotSerializer(serializer: import('pretty-format').Plugin): void; - } - } -} diff --git a/packages/apollo-federation/src/snapshotSerializers/selectionSetSerializer.ts b/packages/apollo-federation/src/snapshotSerializers/selectionSetSerializer.ts deleted file mode 100644 index 33dfba417ac..00000000000 --- a/packages/apollo-federation/src/snapshotSerializers/selectionSetSerializer.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { print, SelectionNode, isSelectionNode } from 'graphql'; -import { Plugin } from 'pretty-format'; - -export default { - test(value: any) { - return ( - Array.isArray(value) && value.length > 0 && value.every(isSelectionNode) - ); - }, - print(selectionNodes: SelectionNode[]): string { - return selectionNodes.map(node => print(node)).join('\n'); - }, -} as Plugin; diff --git a/packages/apollo-federation/src/snapshotSerializers/typeSerializer.ts b/packages/apollo-federation/src/snapshotSerializers/typeSerializer.ts deleted file mode 100644 index 7b78e18cac1..00000000000 --- a/packages/apollo-federation/src/snapshotSerializers/typeSerializer.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { isNamedType, GraphQLNamedType, printType } from 'graphql'; -import { Plugin } from 'pretty-format'; - -export default { - test(value: any) { - return value && isNamedType(value); - }, - print(value: GraphQLNamedType) { - return printType(value); - }, -} as Plugin; diff --git a/packages/apollo-federation/src/types.ts b/packages/apollo-federation/src/types.ts deleted file mode 100644 index 453835b5943..00000000000 --- a/packages/apollo-federation/src/types.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - GraphQLFieldConfig, - GraphQLString, - GraphQLUnionType, - GraphQLObjectType, - GraphQLScalarType, - GraphQLNonNull, - GraphQLList, - GraphQLType, - GraphQLNamedType, - isNamedType, - GraphQLResolveInfo, - isObjectType, -} from 'graphql'; -import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue'; - -export const EntityType = new GraphQLUnionType({ - name: '_Entity', - types: [], -}); - -export const ServiceType = new GraphQLObjectType({ - name: '_Service', - fields: { - sdl: { - type: GraphQLString, - description: - 'The sdl representing the federated service capabilities. Includes federation directives, removes federation types, and includes rest of full schema after schema directives have been applied', - }, - }, -}); - -export const AnyType = new GraphQLScalarType({ - name: '_Any', - serialize(value) { - return value; - }, -}); - -function isPromise(value: PromiseOrValue): value is Promise { - return Boolean(value && 'then' in value && typeof value.then === 'function'); -} - -function addTypeNameToPossibleReturn( - maybeObject: null | T, - typename: string, -): null | T & { __typename: string } { - if (maybeObject !== null && typeof maybeObject === 'object') { - Object.defineProperty(maybeObject, '__typename', { - value: typename, - }); - } - return maybeObject as null | T & { __typename: string }; -} - -export type GraphQLReferenceResolver = ( - reference: object, - context: TContext, - info: GraphQLResolveInfo, -) => any; - -declare module 'graphql/type/definition' { - interface GraphQLObjectType { - resolveReference?: GraphQLReferenceResolver; - } - - interface GraphQLObjectTypeConfig { - resolveReference?: GraphQLReferenceResolver; - } -} - -export const entitiesField: GraphQLFieldConfig = { - type: new GraphQLNonNull(new GraphQLList(EntityType)), - args: { - representations: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AnyType))), - }, - }, - resolve(_source, { representations }, context, info) { - return representations.map((reference: { __typename: string } & object) => { - const { __typename } = reference; - - const type = info.schema.getType(__typename); - if (!type || !isObjectType(type)) { - throw new Error( - `The _entities resolver tried to load an entity for type "${__typename}", but no object type of that name was found in the schema`, - ); - } - - const resolveReference = type.resolveReference - ? type.resolveReference - : function defaultResolveReference() { - return reference; - }; - - // FIXME somehow get this to show up special in Engine traces? - const result = resolveReference(reference, context, info); - - if (isPromise(result)) { - return result.then((x: any) => - addTypeNameToPossibleReturn(x, __typename), - ); - } - - return addTypeNameToPossibleReturn(result, __typename); - }); - }, -}; - -export const serviceField: GraphQLFieldConfig = { - type: new GraphQLNonNull(ServiceType), -}; - -export const federationTypes: GraphQLNamedType[] = [ - ServiceType, - AnyType, - EntityType, -]; - -export function isFederationType(type: GraphQLType): boolean { - return ( - isNamedType(type) && federationTypes.some(({ name }) => name === type.name) - ); -} diff --git a/packages/apollo-federation/tsconfig.json b/packages/apollo-federation/tsconfig.json deleted file mode 100644 index 704463de35b..00000000000 --- a/packages/apollo-federation/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist", - "lib": ["es2017", "es2019.array", "esnext.asynciterable"], - }, - "include": ["src/**/*"], - "exclude": ["**/__tests__", "**/__mocks__"], - "references": [ - { "path": "../apollo-federation-integration-testsuite" } - ] -} diff --git a/packages/apollo-gateway/.npmignore b/packages/apollo-gateway/.npmignore deleted file mode 100644 index a165046d359..00000000000 --- a/packages/apollo-gateway/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -* -!src/**/* -!dist/**/* -dist/**/*.test.* -!package.json -!README.md diff --git a/packages/apollo-gateway/CHANGELOG.md b/packages/apollo-gateway/CHANGELOG.md deleted file mode 100644 index 49138fdb476..00000000000 --- a/packages/apollo-gateway/CHANGELOG.md +++ /dev/null @@ -1,234 +0,0 @@ -# CHANGELOG for `@apollo/gateway` - -## vNEXT - -> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. - -- _Nothing yet! Stay tuned!_ - -## v0.20.0 - -- Only changes in the similarly versioned `@apollo/federation` package. - -## v0.19.1 - -- Only changes in the similarly versioned `@apollo/federation` package. - -## v0.19.0 - -- Only changes in the similarly versioned `@apollo/federation` package. - -## v0.18.1 - -- __FIX__: Pass null required fields correctly within the parent object to resolvers. When a composite field was null, it would sometimes be expanded into an object with all null subfields and passed to the resolver. This fix prevents this expansion and sets the field to null, as originally intended. [PR #4157](https://github.com/apollographql/apollo-server/pull/4157) -- __FIX__: Prevent gateway from entering an inoperable state after an initial configuration load failure. [PR #4277](https://github.com/apollographql/apollo-server/pull/4277) - -## v0.18.0 - -- The `RemoteGraphQLDataSource`'s `didEncounterError` method will now receive [`Response`](https://github.com/apollographql/apollo-server/blob/43470d6561bee31101f3afc56bdd154db3f92b30/packages/apollo-server-env/src/fetch.d.ts#L98-L111) as the third argument when it is available, making its signature `(error: Error, fetchRequest: Request, fetchResponse?: Response)`. This compliments the existing [`Request`](https://github.com/apollographql/apollo-server/blob/43470d6561bee31101f3afc56bdd154db3f92b30/packages/apollo-server-env/src/fetch.d.ts#L37-L45) type it was already receiving. Both of these types are [HTTP WHATWG Fetch API](https://fetch.spec.whatwg.org/) types, not `GraphQLRequest`, `GraphQLResponse` types. - -## v0.17.0 - -- __BREAKING__: Move federation metadata from custom objects on schema nodes over to the `extensions` field on schema nodes which are intended for metadata. This is a breaking change because it narrows the `graphql` peer dependency from `^14.0.2` to `^14.5.0` which is when [`extensions` were introduced](https://github.com/graphql/graphql-js/pull/2097) for all Type System objects. [PR #4313](https://github.com/apollographql/apollo-server/pull/4313) - -## v0.16.11 - -- Only changes in the similarly versioned `@apollo/federation` package. - -## v0.16.10 - -- The default branch of the repository has been changed to `main`. As this changed a number of references in the repository's `package.json` and `README.md` files (e.g., for badges, links, etc.), this necessitates a release to publish those changes to npm. [PR #4302](https://github.com/apollographql/apollo-server/pull/4302) -- __FIX__: The cache implementation for the HTTP-fetcher which is used when communicating with the Apollo Registry when the gateway is configured to use [managed federation](https://www.apollographql.com/docs/graph-manager/managed-federation/overview/) will no longer write to its cache when it receives a 304 response. This is necessary since such a response indicates that the cache used to conditionally make the request must already be present. This does not affect GraphQL requests at runtime, only the polling and fetching mechanism for retrieving composed schemas under manged federation. [PR #4325](https://github.com/apollographql/apollo-server/pull/4325) -- __FIX__: The `mergeFieldNodeSelectionSets` method no longer mutates original FieldNode objects. Before, it was updating the selection set of the original object, corrupting the data accross requests. - -## v0.16.9 - -- Only changes in the similarly versioned `@apollo/federation` package. - -## v0.16.7 - -- Bumped the version of `apollo-server-core`, but no other changes! - -## v0.16.6 - -- Only changes in the similarly versioned `@apollo/federation` package. - -## v0.16.5 - -- Only changes in the similarly versioned `@apollo/federation` package. - -## v0.16.4 - -- __NEW__: Provide the `requestContext` as an argument to the experimental callback function `experimental_didResolveQueryPlan`. [#4173](https://github.com/apollographql/apollo-server/pull/4173) - -## v0.16.3 - -- This updates a dependency of `apollo-server-core` that is only used for its TypeScript typings, not for any runtime dependencies. The reason for the upgrade is that the `apollo-server-core` package (again, used only for types!) was affected by a GitHub Security Advisory. [See the related `CHANGELOG.md` for Apollo Server for more details, including a link to the advisory](https://github.com/apollographql/apollo-server/blob/354d9910e1c87af93c7d50263a28554b449e48db/CHANGELOG.md#v2142). - -## v0.16.2 - -- __FIX__: Collapse nested required fields into a single body in the query plan. Before, some nested fields' selection sets were getting split, causing some of their subfields to be dropped when executing the query. This fix collapses the split selection sets into one. [#4064](https://github.com/apollographql/apollo-server/pull/4064) - -## v0.16.1 - -- __NEW__: Provide the ability to pass a custom `fetcher` during `RemoteGraphQLDataSource` construction to be used when executing operations against downstream services. Providing a custom `fetcher` may be necessary to accommodate more advanced needs, e.g., configuring custom TLS certificates for internal services. [PR #4149](https://github.com/apollographql/apollo-server/pull/4149) - - The `fetcher` specified should be a compliant implementor of the [Fetch API standard](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). This addition compliments, though is still orthognonal to, similar behavior originally introduced in [#3783](https://github.com/apollographql/apollo-server/pull/3783), which allowed customization of the implementation used to fetch _gateway configuration and federated SDL from services_ in managed and unmanaged modes, but didn't affect the communication that takes place during _operation execution_. - - For now, the default `fetcher` will remain the same ([`node-fetch`](https://npm.im/node-fetch)) implementation. A future major-version bump will update it to be consistent with other feature-rich implementations of the Fetch API which are used elsewhere in the Apollo Server stack where we use [`make-fetch-happen`](https://npm.im/make-fetch-happen). In all likelihood, `ApolloGateway` will pass its own `fetcher` to the `RemoteGraphQLDataSource` during service initialization. - -## v0.16.0 - -- __BREAKING__: Use a content delivery network for managed configuration, fetch storage secrets and composition configuration from different domains: https://storage-secrets.api.apollographql.com and https://federation.api.apollographql.com. Please mind any firewall for outgoing traffic. [#4080](https://github.com/apollographql/apollo-server/pull/4080) - -## v0.15.1 - -- __FIX__: Correctly handle unions with nested conditions that have no `possibleTypes` [#4071](https://github.com/apollographql/apollo-server/pull/4071) -- __FIX__: Normalize root operation types when reporting to Apollo Graph Manager. Federation always uses the default names `Query`, `Mutation`, and `Subscription` for root operation types even if downstream services choose different names; now we properly normalize traces received from downstream services in the same way. [#4100](https://github.com/apollographql/apollo-server/pull/4100) - -## v0.15.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/e37384a49b2bf474eed0de3e9f4a1bebaeee64c7) - -- __BREAKING__: Drop support for Node.js 8 and Node.js 10. This is being done primarily for performance gains which stand to be seen by transpiling to a newer ECMAScript target. For more details, see the related PR. [#4031](https://github.com/apollographql/apollo-server/pull/4031) -- __Performance:__ Cache stringified representations of downstream query bodies within the query plan to address performance cost incurred by repeatedly `print`ing the same`DocumentNode`s with the `graphql` printer. This improvement is more pronounced on larger documents. [PR #4018](https://github.com/apollographql/apollo-server/pull/4018) -- __Deprecation:__ Deprecated the `ENGINE_API_KEY` environment variable in favor of its new name, `APOLLO_KEY`. The new name mirrors the name used within Apollo Graph Manager. Aside from the rename, the functionality remains otherwise identical. Continued use of `ENGINE_API_KEY` will result in deprecation warnings being printed to the server console. Support for `ENGINE_API_KEY` will be removed in a future, major update. [#3923](https://github.com/apollographql/apollo-server/pull/3923) -- __Deprecation:__ Deprecated the `APOLLO_SCHEMA_TAG` environment variable in favor of its new name, `APOLLO_GRAPH_VARIANT`. The new name mirrors the name used within Apollo Graph Manager. Aside from the rename, the functionality remains otherwise identical. Use of the now-deprecated name will result in a deprecation warning being printed to the server console. Support will be removed entirely in a future, major update. To avoid misconfiguration, runtime errors will be thrown if the new and deprecated versions are _both_ set. [#3855](https://github.com/apollographql/apollo-server/pull/3855) -- Add inadvertently excluded `apollo-server-errors` runtime dependency. [#3927](https://github.com/apollographql/apollo-server/pull/3927) - -## v0.14.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/b898396e9fcd3b9092b168f9aac8466ca186fa6b) - -- __FIX__: Resolve condition which surfaced in `0.14.0` which prevented loading the configuration using managed federation. [PR #3979](https://github.com/apollographql/apollo-server/pull/3979) - -## v0.14.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/71a3863f59f4ab2c9052c316479d94c6708c4309) - -- Several previously unhandled Promise rejection errors stemming from, e.g. connectivity, failures when communicating with Apollo Graph Manager within asynchronous code are now handled. [PR #3811](https://github.com/apollographql/apollo-server/pull/3811) -- Provide a more helpful error message when encountering expected errors. [PR #3811](https://github.com/apollographql/apollo-server/pull/3811) -- General improvements and clarity to error messages and logging. [PR #3811](https://github.com/apollographql/apollo-server/pull/3811) -- Warn of a possible misconfiguration when local service configuration is provided (via `serviceList` or `localServiceList`) and a remote Apollo Graph Manager configuration is subsequently found as well. [PR #3868](https://github.com/apollographql/apollo-server/pull/3868) -- During composition, the unavailability of a downstream service in unmanaged federation mode will no longer result in a partially composed schema which merely lacks the types provided by the downed service. This prevents unexpected validation errors for clients querying a graph which lacks types which were merely unavailable during the initial composition but were intended to be part of the graph. [PR #3867](https://github.com/apollographql/apollo-server/pull/3867) -- Support providing a custom logger implementation (e.g. [`winston`](https://npm.im/winston), [`bunyan`](https://npm.im/bunyan), etc.) to capture gateway-sourced console output. This allows the use of existing, production logging facilities or the possibiltiy to use advanced structure in logging, such as console output which is encapsulated in JSON. The same PR that introduces this support also introduces a `logger` property to the `GraphQLRequestContext` that is exposed to `GraphQLDataSource`s and Apollo Server plugins, making it possible to attach additional properties (as supported by the logger implementation) to specific requests, if desired, by leveraging custom implementations in those components respectively. When not provided, these will still output to `console`. [PR #3894](https://github.com/apollographql/apollo-server/pull/3894) -- Drop use of `loglevel-debug`. This removes the very long date and time prefix in front of each log line and also the support for the `DEBUG=apollo-gateway:` environment variable. Both of these were uncommonly necessary or seldom used (with the environment variable also being undocumented). The existing behavior can be preserved by providing a `logger` that uses `loglevel-debug`, if desired, and more details can be found in the PR. [PR #3896](https://github.com/apollographql/apollo-server/pull/3896) -- Fix Typescript generic typing for datasource contexts [#3865](https://github.com/apollographql/apollo-server/pull/3865) This is a fix for the `TContext` typings of the gateway's exposed `GraphQLDataSource` implementations. In their current form, they don't work as intended, or in any manner that's useful for typing the `context` property throughout the class methods. This introduces a type argument `TContext` to the class itself (which defaults to `Record` for existing implementations) and removes the non-operational type arguments on the class methods themselves. -- Implement retry logic for requests to GCS [PR #3836](https://github.com/apollographql/apollo-server/pull/3836) Note: coupled with this change is a small alteration in how the gateway polls GCS for updates in managed mode. Previously, the tick was on a specific interval. Now, every tick starts after the round of fetches to GCS completes. For more details, see the linked PR. -- Gateway issues health checks to downstream services via `serviceHealthCheck` configuration option. Note: expected behavior differs between managed and unmanaged federation. See PR for new test cases and documentation. [#3930](https://github.com/apollographql/apollo-server/pull/3930) - - -## v0.13.2 - -- __BREAKING__: The behavior and signature of `RemoteGraphQLDataSource`'s `didReceiveResponse` method has been changed. No changes are necessary _unless_ your implementation has overridden the default behavior of this method by either extending the class and overriding the method or by providing `didReceiveResponse` as a parameter to the `RemoteGraphQLDataSource`'s constructor options. Implementations which have provided their own `didReceiveResponse` using either of these methods should view the PR linked here for details on what has changed. [PR #3743](https://github.com/apollographql/apollo-server/pull/3743) -- __NEW__: Setting the `apq` option to `true` on the `RemoteGraphQLDataSource` will enable the use of [automated persisted queries (APQ)](https://www.apollographql.com/docs/apollo-server/performance/apq/) when sending queries to downstream services. Depending on the complexity of queries sent to downstream services, this technique can greatly reduce the size of the payloads being transmitted over the network. Downstream implementing services must also support APQ functionality to participate in this feature (Apollo Server does by default unless it has been explicitly disabled). As with normal APQ behavior, a downstream server must have received and registered a query once before it will be able to serve an APQ request. [#3744](https://github.com/apollographql/apollo-server/pull/3744) -- __NEW__: Experimental feature: compress downstream requests via generated fragments [#3791](https://github.com/apollographql/apollo-server/pull/3791) This feature enables the gateway to generate fragments for queries to downstream services in order to minimize bytes over the wire and parse time. This can be enabled via the gateway config by setting `experimental_autoFragmentization: true`. It is currently disabled by default. -- Introduce `make-fetch-happen` package. Remove `cachedFetcher` in favor of the caching implementation provided by this package. [#3783](https://github.com/apollographql/apollo-server/pull/3783/files) - -## v0.12.1 - -- Update to include [fixes from `@apollo/federation`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-federation/CHANGELOG.md). - -## v0.12.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/9c0aa1e661ccc2c5a1471b781102637dd47e21b1) - -- Reduce interface expansion for types contained to a single service [#3582](https://github.com/apollographql/apollo-server/pull/3582) -- Instantiate one `CachedFetcher` per gateway instance. This resolves a condition where multiple federated gateways would utilize the same cache store could result in an `Expected undefined to be a GraphQLSchema` error. [#3704](https://github.com/apollographql/apollo-server/pull/3704) -- Gateway: minimize downstream request size [#3737](https://github.com/apollographql/apollo-server/pull/3737) -- experimental: Allow configuration of the query plan store by introducing an `experimental_approximateQueryPlanStoreMiB` property to the `ApolloGateway` constructor options which overrides the default cache size of 30MiB. [#3755](https://github.com/apollographql/apollo-server/pull/3755) - -## v0.11.6 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/0743d6b2f1737758cf09e80d2086917772bc00c9) - -- Fix onSchemaChange callbacks for unmanaged configs [#3605](https://github.com/apollographql/apollo-server/pull/3605) - -## v0.11.4 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/a0a60e73e04e913d388de8324f7d17e4406deea2) - - * Gateway over-merging fields of unioned types [#3581](https://github.com/apollographql/apollo-server/pull/3581) - -## v0.11.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/93002737d53dd9a50b473ab9cef14849b3e539aa) - -- Begin supporting executable directives in federation [#3464](https://github.com/apollographql/apollo-server/pull/3464) - -## v0.10.8 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/5d94e986f04457ec17114791ee6db3ece4213dd8) - -- Fix Gateway / Playground Query Plan view [#3418](https://github.com/apollographql/apollo-server/pull/3418) -- Gateway schema change listener bug + refactor [#3411](https://github.com/apollographql/apollo-server/pull/3411) introduces a change to the `experimental_didUpdateComposition` hook and `experimental_pollInterval` configuration behavior. - 1. Previously, the `experimental_didUpdateComposition` hook wouldn't be reliably called unless the `experimental_pollInterval` was set. If it _was_ called, it was sporadic and didn't necessarily mark the timing of an actual composition update. After this change, the hook is called on a successful composition update. - 2. The `experimental_pollInterval` configuration option now affects both the GCS polling interval when gateway is configured for managed federation, as well as the polling interval of services. The former being newly introduced behavior. -- Gateway cached DataSource bug [#3412](https://github.com/apollographql/apollo-server/pull/3412) introduces a fix for managed federation users where `DataSource`s wouldn't update correctly if a service's url changed. This bug was introduced with heavier DataSource caching in [#3388](https://github.com/apollographql/apollo-server/pull/3388). By inspecting the `url` as well, `DataSource`s will now update correctly when a composition update occurs. -- Gateway - don't log updates on startup [#3421](https://github.com/apollographql/apollo-server/pull/3421) Fine tune gateway startup logging - on load, instead of logging an "update", log the service id, variant, and mode in which gateway is running. - -## v0.10.7 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/fc7462ec5f8604bd6cba99aa9a377a9b8e045566) - -- Add export for experimental observability functions types. [#3371](https://github.com/apollographql/apollo-server/pull/3371) -- Fix double instantiation of DataSources [#3388](https://github.com/apollographql/apollo-server/pull/3388) - -## v0.10.6 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/aa200ce24b834320fc79d2605dac340b37d3e434) - -- Fix debug query plan logging [#3376](https://github.com/apollographql/apollo-server/pull/3376) -- Add `context` object to `GraphQLDataSource.didReceiveResponse` arguments [#3360](https://github.com/apollographql/apollo-server/pull/3360) - -## v0.10.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/029c8dca3af812ee70589cdb6de749df3d2843d8) - -- Make service definition cache local to ApolloGateway object [#3191](https://github.com/apollographql/apollo-server/pull/3191) -- Fix value type behavior within composition and execution [#3182](https://github.com/apollographql/apollo-server/pull/3182) -- Validate variables at the gateway level [#3213](https://github.com/apollographql/apollo-server/pull/3213) - -## v0.9.1 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/a1c41152a35c837af27d1dee081fc273de07a28e) - -- Optimize buildQueryPlan when two FetchGroups are on the same service [#3135](https://github.com/apollographql/apollo-server/pull/3135) -- Construct and use RemoteGraphQLDataSource to issue introspection query to Federated Services [#3120](https://github.com/apollographql/apollo-server/pull/3120) - -## v0.9.0 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/99f78c6782bce170186ba6ef311182a8c9f281b7) - -- Add experimental observability functions [#3110](https://github.com/apollographql/apollo-server/pull/3110) - -## v0.8.2 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/b0a9ce0615d19b7241e64883b5d5d7730cc13fcb) - -- Handle `null` @requires selections correctly during execution [#3138](https://github.com/apollographql/apollo-server/pull/3138) - -## v0.6.13 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/a06594117dbbf1e8abdb7b366b69a94ab808b065) - -- Proxy errors from downstream services [#3019](https://github.com/apollographql/apollo-server/pull/3019) -- Handle schema defaultVariables correctly within downstream fetches [#2963](https://github.com/apollographql/apollo-server/pull/2963) - -## v0.6.12 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/5974b2ce405a06bc331230400b9073f6381738d3) - -- Fix `@requires` bug preventing array and null values. [PR #2928](https://github.com/apollographql/apollo-server/pull/2928) - -## v0.6.5 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/9dcfe6f91fa7b4187a644efe1522cf444ffc1251) - -- Relax constraints of root operation type names in validation [#2783](ttps://github.com/apollographql/apollo-server/pull/2783) - -## v0.6.2 - -> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/e113127b1ff9802de3bc5574bcae55256f0ef656) - -- Resolve an issue with \__proto__ pollution in deepMerge() [#2779](https://github.com/apollographql/apollo-server/pull/2779) diff --git a/packages/apollo-gateway/LICENSE.md b/packages/apollo-gateway/LICENSE.md deleted file mode 100644 index dce88b77022..00000000000 --- a/packages/apollo-gateway/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License - -Copyright (c) 2019 Meteor Development Group, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/apollo-gateway/README.md b/packages/apollo-gateway/README.md deleted file mode 100644 index 53904a29b1b..00000000000 --- a/packages/apollo-gateway/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Apollo Gateway - -This package provides utilities for combining multiple GraphQL microservices into a single GraphQL endpoint. - -Each microservice should implement the [federation schema specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). This can be done either through [Apollo Federation](https://github.com/apollographql/apollo-server/tree/main/packages/apollo-federation) or a variety of other open source products. - -For complete documentation, see the [Apollo Gateway API reference](https://www.apollographql.com/docs/apollo-server/api/apollo-gateway/). - -## Usage - -```js -const { ApolloServer } = require("apollo-server"); -const { ApolloGateway } = require("@apollo/gateway"); - -const gateway = new ApolloGateway({ - serviceList: [ - { name: "accounts", url: "http://localhost:4001/graphql" }, - // List of federation-capable GraphQL endpoints... - ] -}); - -const server = new ApolloServer({ gateway }); - -server.listen().then(({ url }) => { - console.log(`🚀 Server ready at ${url}`); -}); -``` diff --git a/packages/apollo-gateway/jest.config.js b/packages/apollo-gateway/jest.config.js deleted file mode 100644 index ae9d3e7cef4..00000000000 --- a/packages/apollo-gateway/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -const path = require('path'); -const config = require('../../jest.config.base'); - -const NODE_MAJOR_VERSION = parseInt( - process.versions.node.split('.', 1)[0], - 10 -); - -const additionalConfig = { - setupFilesAfterEnv: [path.resolve(__dirname, './src/__tests__/testSetup.ts')], - testPathIgnorePatterns: [ - ...config.testPathIgnorePatterns, - ...NODE_MAJOR_VERSION < 12 ? [""] : [] - ] -}; - -module.exports = Object.assign(Object.create(null), config, additionalConfig); diff --git a/packages/apollo-gateway/package.json b/packages/apollo-gateway/package.json deleted file mode 100644 index e9d1fe3254c..00000000000 --- a/packages/apollo-gateway/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "@apollo/gateway", - "version": "0.20.0", - "description": "Apollo Gateway", - "author": "Apollo ", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "keywords": [ - "GraphQL", - "Apollo", - "Server", - "Javascript" - ], - "engines": { - "node": ">=12.13.0" - }, - "license": "MIT", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@apollo/federation": "file:../apollo-federation", - "@types/node-fetch": "2.5.4", - "apollo-engine-reporting-protobuf": "file:../apollo-engine-reporting-protobuf", - "apollo-env": "^0.6.1", - "apollo-graphql": "^0.6.0", - "apollo-server-caching": "file:../apollo-server-caching", - "apollo-server-core": "file:../apollo-server-core", - "apollo-server-env": "file:../apollo-server-env", - "apollo-server-errors": "file:../apollo-server-errors", - "apollo-server-types": "file:../apollo-server-types", - "graphql-extensions": "file:../graphql-extensions", - "loglevel": "^1.6.1", - "make-fetch-happen": "^8.0.0", - "pretty-format": "^26.0.0" - }, - "peerDependencies": { - "graphql": "^14.5.0 || ^15.0.0" - } -} diff --git a/packages/apollo-gateway/src/FieldSet.ts b/packages/apollo-gateway/src/FieldSet.ts deleted file mode 100644 index 25f613d3e80..00000000000 --- a/packages/apollo-gateway/src/FieldSet.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { - FieldNode, - getNamedType, - GraphQLCompositeType, - GraphQLField, - isCompositeType, - Kind, - SelectionNode, - SelectionSetNode, - GraphQLObjectType, -} from 'graphql'; -import { getResponseName } from './utilities/graphql'; -import { partition, groupBy } from './utilities/array'; - -export interface Field< - TParent extends GraphQLCompositeType = GraphQLCompositeType -> { - scope: Scope; - fieldNode: FieldNode; - fieldDef: GraphQLField; -} - -export interface Scope { - parentType: TParent; - possibleTypes: ReadonlyArray; - enclosingScope?: Scope; -} - -export type FieldSet = Field[]; - -export function printFields(fields?: FieldSet) { - if (!fields) return '[]'; - return ( - '[' + - fields - .map(field => `"${field.scope.parentType.name}.${field.fieldDef.name}"`) - .join(', ') + - ']' - ); -} - -export function matchesField(field: Field) { - // TODO: Compare parent type and arguments - return (otherField: Field) => { - return field.fieldDef.name === otherField.fieldDef.name; - }; -} - -export const groupByResponseName = groupBy(field => - getResponseName(field.fieldNode) -); - -export const groupByParentType = groupBy( - field => field.scope.parentType, -); - -export function selectionSetFromFieldSet( - fields: FieldSet, - parentType?: GraphQLCompositeType, -): SelectionSetNode { - return { - kind: Kind.SELECTION_SET, - selections: Array.from(groupByParentType(fields)).flatMap( - ([typeCondition, fieldsByParentType]: [GraphQLCompositeType, FieldSet]) => - wrapInInlineFragmentIfNeeded( - Array.from(groupByResponseName(fieldsByParentType).values()).map( - fieldsByResponseName => { - return combineFields(fieldsByResponseName) - .fieldNode; - }, - ), - typeCondition, - parentType, - ), - ), - }; -} - -function wrapInInlineFragmentIfNeeded( - selections: SelectionNode[], - typeCondition: GraphQLCompositeType, - parentType?: GraphQLCompositeType, -): SelectionNode[] { - return typeCondition === parentType - ? selections - : [ - { - kind: Kind.INLINE_FRAGMENT, - typeCondition: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: typeCondition.name, - }, - }, - selectionSet: { kind: Kind.SELECTION_SET, selections }, - }, - ]; -} - -function combineFields( - fields: FieldSet, -): Field { - const { scope, fieldNode, fieldDef } = fields[0]; - const returnType = getNamedType(fieldDef.type); - - if (isCompositeType(returnType)) { - return { - scope, - fieldNode: { - ...fieldNode, - selectionSet: mergeSelectionSets(fields.map(field => field.fieldNode)), - }, - fieldDef, - }; - } else { - return { scope, fieldNode, fieldDef }; - } -} - -function mergeSelectionSets(fieldNodes: FieldNode[]): SelectionSetNode { - const selections: SelectionNode[] = []; - - for (const fieldNode of fieldNodes) { - if (!fieldNode.selectionSet) continue; - - selections.push(...fieldNode.selectionSet.selections); - } - - return { - kind: 'SelectionSet', - selections: mergeFieldNodeSelectionSets(selections), - }; -} - -function mergeFieldNodeSelectionSets( - selectionNodes: SelectionNode[], -): SelectionNode[] { - const [fieldNodes, fragmentNodes] = partition( - selectionNodes, - (node): node is FieldNode => node.kind === Kind.FIELD, - ); - - const [aliasedFieldNodes, nonAliasedFieldNodes] = partition( - fieldNodes, - node => !!node.alias, - ); - - const mergedFieldNodes = Array.from( - groupBy((node: FieldNode) => node.name.value)( - nonAliasedFieldNodes, - ).values(), - ).map((nodesWithSameName) => { - const node = { ...nodesWithSameName[0] }; - if (node.selectionSet) { - node.selectionSet = { - ...node.selectionSet, - selections: mergeFieldNodeSelectionSets( - nodesWithSameName.flatMap( - (node) => node.selectionSet?.selections || [], - ), - ), - }; - } - return node; - }); - - return [...mergedFieldNodes, ...aliasedFieldNodes, ...fragmentNodes]; -} diff --git a/packages/apollo-gateway/src/QueryPlan.ts b/packages/apollo-gateway/src/QueryPlan.ts deleted file mode 100644 index 26219e75a48..00000000000 --- a/packages/apollo-gateway/src/QueryPlan.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - FragmentDefinitionNode, - GraphQLSchema, - OperationDefinitionNode, - Kind, - SelectionNode as GraphQLJSSelectionNode, -} from 'graphql'; -import prettyFormat from 'pretty-format'; -import { queryPlanSerializer, astSerializer } from './snapshotSerializers'; - -export type ResponsePath = (string | number)[]; - -export type FragmentMap = { [fragmentName: string]: FragmentDefinitionNode }; - -export type OperationContext = { - schema: GraphQLSchema; - operation: OperationDefinitionNode; - fragments: FragmentMap; -}; - -export interface QueryPlan { - kind: 'QueryPlan'; - node?: PlanNode; -} - -export type PlanNode = SequenceNode | ParallelNode | FetchNode | FlattenNode; - -export interface SequenceNode { - kind: 'Sequence'; - nodes: PlanNode[]; -} - -export interface ParallelNode { - kind: 'Parallel'; - nodes: PlanNode[]; -} - -export interface FetchNode { - kind: 'Fetch'; - serviceName: string; - variableUsages?: string[]; - requires?: QueryPlanSelectionNode[]; - operation: string; -} - -export interface FlattenNode { - kind: 'Flatten'; - path: ResponsePath; - node: PlanNode; -} - -/** - * SelectionNodes from GraphQL-js _can_ have a FragmentSpreadNode - * but this SelectionNode is specifically typing the `requires` key - * in a built query plan, where there can't be FragmentSpreadNodes - * since that info is contained in the `FetchNode.operation` - */ -export type QueryPlanSelectionNode = QueryPlanFieldNode | QueryPlanInlineFragmentNode; - -export interface QueryPlanFieldNode { - readonly kind: 'Field'; - readonly alias?: string; - readonly name: string; - readonly selections?: QueryPlanSelectionNode[]; -} - -export interface QueryPlanInlineFragmentNode { - readonly kind: 'InlineFragment'; - readonly typeCondition?: string; - readonly selections: QueryPlanSelectionNode[]; -} - -export function serializeQueryPlan(queryPlan: QueryPlan) { - return prettyFormat(queryPlan, { - plugins: [queryPlanSerializer, astSerializer], - }); -} - -export function getResponseName(node: QueryPlanFieldNode): string { - return node.alias ? node.alias : node.name; -} - -/** - * Converts a GraphQL-js SelectionNode to our newly defined SelectionNode - * - * This function is used to remove the unneeded pieces of a SelectionSet's - * `.selections`. It is only ever called on a query plan's `requires` field, - * so we can guarantee there won't be any FragmentSpreads passed in. That's why - * we can ignore the case where `selection.kind === Kind.FRAGMENT_SPREAD` - */ -export const trimSelectionNodes = ( - selections: readonly GraphQLJSSelectionNode[], -): QueryPlanSelectionNode[] => { - /** - * Using an array to push to instead of returning value from `selections.map` - * because TypeScript thinks we can encounter a `Kind.FRAGMENT_SPREAD` here, - * so if we mapped the array directly to the return, we'd have to `return undefined` - * from one branch of the map and then `.filter(Boolean)` on that returned - * array - */ - const remapped: QueryPlanSelectionNode[] = []; - - selections.forEach((selection) => { - if (selection.kind === Kind.FIELD) { - remapped.push({ - kind: Kind.FIELD, - name: selection.name.value, - selections: - selection.selectionSet && - trimSelectionNodes(selection.selectionSet.selections), - }); - } - if (selection.kind === Kind.INLINE_FRAGMENT) { - remapped.push({ - kind: Kind.INLINE_FRAGMENT, - typeCondition: selection.typeCondition?.name.value, - selections: trimSelectionNodes(selection.selectionSet.selections), - }); - } - }); - - return remapped; -}; diff --git a/packages/apollo-gateway/src/__tests__/.gitkeep b/packages/apollo-gateway/src/__tests__/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/apollo-gateway/src/__tests__/CucumberREADME.md b/packages/apollo-gateway/src/__tests__/CucumberREADME.md deleted file mode 100644 index 25d4dc90438..00000000000 --- a/packages/apollo-gateway/src/__tests__/CucumberREADME.md +++ /dev/null @@ -1,96 +0,0 @@ -# Query Plan Tests - -## Introduction - -There are two files used to test the query plan builder: - -1. [build-query-plan.feature](./build-query-plan.feature): Programming-language agnostic files written in a format called [Gherkin](https://cucumber.io/docs/gherkin/reference/) for [Cucumber](https://cucumber.io/). -2. [queryPlanCucumber.test.ts](./queryPlanCucumber.test.ts): The implementation which provides coverage for the Gherkin-specified behavior. - -> If you're not familiar with Cucumber or BDD, check out [this video](https://youtu.be/lC0jzd8sGIA) for a great introduction to the concepts involved. Cucumber has test runners in multiple languages, allowing a test spec to be written in plain English and then individual implementations of the test suite can describe how they would like tests to be run for their specific implementation. For Java, Kotlin, Ruby, and JavaScript, Cucumber even has a [10-minute tutorial](https://cucumber.io/docs/guides/10-minute-tutorial/) to help get started. - - -## Scenarios - -_Scenarios_ are Cucumber's test cases. Each scenario should contain the instructions for a single kind of test. - -## Steps - -Cucumber tests (scenarios) are made up of `steps`. Each step can be prefixed with a "`Given`", "`When`", or "`Then`" step, which when all provided, must occur in precisely that order. These stages represent test _preconditions_, test _execution_, and test _expectations_, respectively. However, tests don't _need_ all 3 of steps! Scenarios can leave off the `When` step when it's not needed. For example, query plan builder tests only have the "Given" and "Then" steps, like so: - -```gherkin -Scenario: should not confuse union types with overlapping field names - Given query - """ - query { - body { - ...on Image { - attributes { - url - } - } - ...on Text { - attributes { - bold - text - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "documents", - "variableUsages": [], - "operation": "{body{__typename ...on Image{attributes{url}}...on Text{attributes{bold text}}}}" - } - } - """ -``` - -There can be multiple of any kind of step using the `And` keyword. In the following example, there are 2 `Given` steps. One represented by the `Given` keyword itself, and another represented with the `And` keyword. - -``` -Given schema A -And schema extension B -Then composed schema should be ... -``` - -Using `And` is especially useful in `Then` steps for testing multiple kinds of expectations. For example, to create a test that looked at a query plan and expected that it called service A and _didn't_ call service B, the test spec would look like this: - -``` -Given service A, B -When querying - """ - query { a } - """ -Then calls service A -And doesn't call service B -``` - -## Writing test integrations - -Cucumber has a test runner for [many different languages](https://cucumber.io/docs/tools/related-tools/) and test frameworks including Java, Ruby, Rust, and many more. Usually, writing an integration for Cucumber looks similar though. You typically need to write instructions for what to with each kind of step. For example, in the example above where querying a service and expecting things of the query plan, we'd need to define 4 different kind of steps, typically with regex matchers (which are simplified here a bit): - -1. `^service *` -2. `^querying` -3. `^calls *` -4. `^doesn't call *` - -Using regex groups, we can extract whatever data we need from the test instructions. For the first pattern, we can use regex to get the service names we want to compose from the given list, and compose them based off a predetermined set of fixtures. - -Gherkin (the language Cucumber tests are written in) has the idea of [arguments](https://cucumber.io/docs/gherkin/reference/#step-arguments) as well, which is what is used in the second step (the `querying...`) step. The query `query { a }` is referred to as an argument to that step, and each cucumber runner has a way of handling arguments, usually as an argument to the handling function. - -In JavaScript, writing a function to handle the `querying` step would look something like this: - -```JavaScript -when(/^querying$/im, (operation) => { - result = execute(services, { query: gql(operation) }); -}); -``` - -It's common in Cucumber execution to keep arguments, variables, and other data globally available to each step. This is either done by a variable scoped above the execution of the steps like in the JavaScript example above or as a mutable "context" passed to each step executor function. This just depends on the language you're working with. The reason this pattern is used is that all steps often need similar data. For example, the `querying` step we defined above needs to know what services are being composed from the `Given` step above to actually execute the operation, and the `Then` steps to follow need to access the execution's result data. diff --git a/packages/apollo-gateway/src/__tests__/build-query-plan.feature b/packages/apollo-gateway/src/__tests__/build-query-plan.feature deleted file mode 100644 index 2fb960b1eca..00000000000 --- a/packages/apollo-gateway/src/__tests__/build-query-plan.feature +++ /dev/null @@ -1,1674 +0,0 @@ -Feature: Build Query Plan - -Scenario: should not confuse union types with overlapping field names - Given query - """ - query { - body { - ...on Image { - attributes { - url - } - } - ...on Text { - attributes { - bold - text - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "documents", - "variableUsages": [], - "operation": "{body{__typename ...on Image{attributes{url}}...on Text{attributes{bold text}}}}" - } - } - """ - -Scenario: should use a single fetch when requesting a root field from one service - Given query - """ - query { - me { - name - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [], - "operation": "{me{name}}" - } - } - """ - -Scenario: should use two independent fetches when requesting root fields from two services - Given query - """ - query { - me { - name - } - topProducts { - name - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Parallel", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [], - "operation": "{me{name}}" - }, - { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "product", - "variableUsages": [], - "operation": "{topProducts{__typename ...on Book{__typename isbn}...on Furniture{name}}}" - }, - { - "kind": "Flatten", - "path": ["topProducts", "@"], - "node": { - "kind": "Fetch", - "serviceName": "books", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" - } - }, - { - "kind": "Flatten", - "path": ["topProducts", "@"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" }, - { "kind": "Field", "name": "title" }, - { "kind": "Field", "name": "year" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" - } - } - ] - } - ] - } - } - """ - -Scenario: should use a single fetch when requesting multiple root fields from the same service - Given query - """ - query { - topProducts { - name - } - product(upc: "1") { - name - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "product", - "variableUsages": [], - "operation": "{topProducts{__typename ...on Book{__typename isbn}...on Furniture{name}}product(upc:\"1\"){__typename ...on Book{__typename isbn}...on Furniture{name}}}" - }, - { - "kind": "Parallel", - "nodes": [ - { - "kind": "Sequence", - "nodes": [ - { - "kind": "Flatten", - "path": ["topProducts", "@"], - "node": { - "kind": "Fetch", - "serviceName": "books", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" - } - }, - { - "kind": "Flatten", - "path": ["topProducts", "@"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" }, - { "kind": "Field", "name": "title" }, - { "kind": "Field", "name": "year" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" - } - } - ] - }, - { - "kind": "Sequence", - "nodes": [ - { - "kind": "Flatten", - "path": ["product"], - "node": { - "kind": "Fetch", - "serviceName": "books", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" - } - }, - { - "kind": "Flatten", - "path": ["product"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" }, - { "kind": "Field", "name": "title" }, - { "kind": "Field", "name": "year" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" - } - } - ] - } - ] - } - ] - } - } - """ - -Scenario: should use a single fetch when requesting relationship subfields from the same service - Given query - """ - query { - topReviews { - body - author { - reviews { - body - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{body author{reviews{body}}}}" - } - } - """ - -Scenario: should use a single fetch when requesting relationship subfields and provided keys from the same service - Given query - """ - query { - topReviews { - body - author { - id - reviews { - body - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{body author{id reviews{body}}}}" - } - } - """ - -Scenario: when requesting an extension field from another service, it should add the field's representation requirements to the parent selection set and use a dependent fetch - Given query - """ - query { - me { - name - reviews { - body - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [], - "operation": "{me{name __typename id}}" - }, - { - "kind": "Flatten", - "path": ["me"], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "id" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}}}}" - } - } - ] - } - } - """ - -Scenario: when requesting an extension field from another service, when the parent selection set is empty, should add the field's requirements to the parent selection set and use a dependent fetch - Given query - """ - query { - me { - reviews { - body - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [], - "operation": "{me{__typename id}}" - }, - { - "kind": "Flatten", - "path": ["me"], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "id" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}}}}" - } - } - ] - } - } - """ - -Scenario: when requesting an extension field from another service, should only add requirements once - Given query - """ - query { - me { - reviews { - body - } - numberOfReviews - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [], - "operation": "{me{__typename id}}" - }, - { - "kind": "Flatten", - "path": ["me"], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "id" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}numberOfReviews}}}" - } - } - ] - } - } - """ - -Scenario: when requesting a composite field with subfields from another service, it should add key fields to the parent selection set and use a dependent fetch - Given query - """ - query { - topReviews { - body - author { - name - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{body author{__typename id}}}" - }, - { - "kind": "Flatten", - "path": ["topReviews", "@", "author"], - "node": { - "kind": "Fetch", - "serviceName": "accounts", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "id" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" - } - } - ] - } - } - """ - -Scenario: when requesting a composite field with subfields from another service, when requesting a field defined in another service which requires a field in the base service, it should add the field provided by base service in first Fetch - Given query - """ - query { - topCars { - retailPrice - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "product", - "variableUsages": [], - "operation": "{topCars{__typename id price}}" - }, - { - "kind": "Flatten", - "path": ["topCars", "@"], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Car", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "id" }, - { "kind": "Field", "name": "price" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Car{retailPrice}}}" - } - } - ] - } - } - """ - -Scenario: when requesting a composite field with subfields from another service, when the parent selection set is empty, it should add key fields to the parent selection set and use a dependent fetch - Given query - """ - query { - topReviews { - author { - name - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{author{__typename id}}}" - }, - { - "kind": "Flatten", - "path": ["topReviews", "@", "author"], - "node": { - "kind": "Fetch", - "serviceName": "accounts", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "id" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" - } - } - ] - } - } - """ - -Scenario: when requesting a relationship field with extension subfields from a different service, it should first fetch the object using a key from the base service and then pass through the requirements - Given query - """ - query { - topReviews { - author { - birthDate - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{author{__typename id}}}" - }, - { - "kind": "Flatten", - "path": ["topReviews", "@", "author"], - "node": { - "kind": "Fetch", - "serviceName": "accounts", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "id" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{birthDate}}}" - } - } - ] - } - } - """ - -Scenario: for abstract types, it should add __typename when fetching objects of an interface type from a service - Given query - """ - query { - topProducts { - price - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "product", - "variableUsages": [], - "operation": "{topProducts{__typename ...on Book{price}...on Furniture{price}}}" - } - } - """ - -Scenario: should break up when traversing an extension field on an interface type from a service - Given query - """ - query { - topProducts { - price - reviews { - body - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "product", - "variableUsages": [], - "operation": "{topProducts{__typename ...on Book{price __typename isbn}...on Furniture{price __typename upc}}}" - }, - { - "kind": "Flatten", - "path": ["topProducts", "@"], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - }, - { - "kind": "InlineFragment", - "typeCondition": "Furniture", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "upc" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{reviews{body}}...on Furniture{reviews{body}}}}" - } - } - ] - } - } - """ - -Scenario: interface fragments should expand into possible types only - Given query - """ - query { - books { - ... on Product { - name - ... on Furniture { - upc - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "books", - "variableUsages": [], - "operation": "{books{__typename isbn title year}}" - }, - { - "kind": "Flatten", - "path": ["books", "@"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" }, - { "kind": "Field", "name": "title" }, - { "kind": "Field", "name": "year" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" - } - } - ] - } - } - """ - -Scenario: interface inside interface should expand into possible types only - Given query - """ - query { - product(upc: "") { - details { - country - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "product", - "variableUsages": [], - "operation": "{product(upc:\"\"){__typename ...on Book{details{country}}...on Furniture{details{country}}}}" - } - } - """ - -Scenario: experimental compression to downstream services should generate fragments internally to downstream requests - Given query - """ - query { - topReviews { - body - author - product { - name - price - details { - country - } - } - } - } - """ - When using autofragmentization - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{...__QueryPlanFragment_1__}}fragment __QueryPlanFragment_1__ on Review{body author product{...__QueryPlanFragment_0__}}fragment __QueryPlanFragment_0__ on Product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}" - }, - { - "kind": "Parallel", - "nodes": [ - { - "kind": "Sequence", - "nodes": [ - { - "kind": "Flatten", - "path": ["topReviews", "@", "product"], - "node": { - "kind": "Fetch", - "serviceName": "books", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" - } - }, - { - "kind": "Flatten", - "path": ["topReviews", "@", "product"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" }, - { "kind": "Field", "name": "title" }, - { "kind": "Field", "name": "year" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" - } - } - ] - }, - { - "kind": "Flatten", - "path": ["topReviews", "@", "product"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Furniture", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "upc" } - ] - }, - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name price details{country}}...on Book{price details{country}}}}" - } - } - ] - } - ] - } - } - """ - -Scenario: experimental compression to downstream services shouldn't generate fragments for selection sets of length 2 or less - Given query - """ - query { - topReviews { - body - author - } - } - """ - When using autofragmentization - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{body author}}" - } - } - """ - -Scenario: experimental compression to downstream services should generate fragments for selection sets of length 3 or greater - Given query - """ - query { - topReviews { - id - body - author - } - } - """ - When using autofragmentization - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{topReviews{...__QueryPlanFragment_0__}}fragment __QueryPlanFragment_0__ on Review{id body author}" - } - } - """ - -Scenario: experimental compression to downstream services should generate fragments correctly when aliases are used - Given query - """ - query { - reviews: topReviews { - content: body - author - product { - name - cost: price - details { - origin: country - } - } - } - } - """ - When using autofragmentization - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [], - "operation": "{reviews:topReviews{...__QueryPlanFragment_1__}}fragment __QueryPlanFragment_1__ on Review{content:body author product{...__QueryPlanFragment_0__}}fragment __QueryPlanFragment_0__ on Product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}" - }, - { - "kind": "Parallel", - "nodes": [ - { - "kind": "Sequence", - "nodes": [ - { - "kind": "Flatten", - "path": ["reviews", "@", "product"], - "node": { - "kind": "Fetch", - "serviceName": "books", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" - } - }, - { - "kind": "Flatten", - "path": ["reviews", "@", "product"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" }, - { "kind": "Field", "name": "title" }, - { "kind": "Field", "name": "year" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" - } - } - ] - }, - { - "kind": "Flatten", - "path": ["reviews", "@", "product"], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Furniture", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "upc" } - ] - }, - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { "kind": "Field", "name": "__typename" }, - { "kind": "Field", "name": "isbn" } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name cost:price details{origin:country}}...on Book{cost:price details{origin:country}}}}" - } - } - ] - } - ] - } - } - """ - -Scenario: should properly expand nested unions with inline fragments - Given query - """ - query { - body { - ... on Image { - ... on Body { - ... on Image { - attributes { - url - } - } - ... on Text { - attributes { - bold - text - } - } - } - } - ... on Text { - attributes { - bold - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "documents", - "variableUsages": [], - "operation": "{body{__typename ...on Image{attributes{url}}...on Text{attributes{bold}}}}" - } - } - """ - -Scenario: deduplicates fields / selections regardless of adjacency and type condition nesting for inline fragments - Given query - """ - query { - body { - ... on Image { - ... on Text { - attributes { - bold - } - } - } - ... on Body { - ... on Text { - attributes { - bold - text - } - } - } - ... on Text { - attributes { - bold - text - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "documents", - "variableUsages": [], - "operation": "{body{__typename ...on Text{attributes{bold text}}}}" - } - } - """ - -Scenario: deduplicates fields / selections regardless of adjacency and type condition nesting for named fragment spreads - Given query - """ - fragment TextFragment on Text { - attributes { - bold - text - } - } - - query { - body { - ... on Image { - ...TextFragment - } - ... on Body { - ...TextFragment - } - ...TextFragment - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "documents", - "variableUsages": [], - "operation": "{body{__typename ...on Text{attributes{bold text}}}}" - } - } - """ - -Scenario: supports basic, single-service mutation - Given query - """ - mutation Login($username: String!, $password: String!) { - login(username: $username, password: $password) { - id - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [ - "username", - "password" - ], - "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){id}}" - } - } - """ - -# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L13 -Scenario: supports mutations with a cross-service request - Given query - """ - mutation Login($username: String!, $password: String!) { - login(username: $username, password: $password) { - reviews { - product { - upc - } - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [ - "username", - "password" - ], - "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" - }, - { - "kind": "Flatten", - "path": [ - "login" - ], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "id" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" - } - }, - { - "kind": "Flatten", - "path": [ - "login", - "reviews", - "@", - "product" - ], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "isbn" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" - } - } - ] - } - } - """ - -# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L48 -Scenario: returning across service boundaries - Given query - """ - mutation Review($upc: String!, $body: String!) { - reviewProduct(upc: $upc, body: $body) { - ... on Furniture { - name - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [ - "upc", - "body" - ], - "operation": "mutation($upc:String!$body:String!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{__typename upc}}}" - }, - { - "kind": "Flatten", - "path": [ - "reviewProduct" - ], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Furniture", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "upc" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name}}}" - } - } - ] - } - } - """ - -# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L75 -Scenario: supports multiple root mutations - Given query - """ - mutation LoginAndReview( - $username: String! - $password: String! - $upc: String! - $body: String! - ) { - login(username: $username, password: $password) { - reviews { - product { - upc - } - } - } - reviewProduct(upc: $upc, body: $body) { - ... on Furniture { - name - } - } - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [ - "username", - "password" - ], - "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" - }, - { - "kind": "Flatten", - "path": [ - "login" - ], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "id" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" - } - }, - { - "kind": "Flatten", - "path": [ - "login", - "reviews", - "@", - "product" - ], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "isbn" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" - } - }, - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [ - "upc", - "body" - ], - "operation": "mutation($upc:String!$body:String!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{__typename upc}}}" - }, - { - "kind": "Flatten", - "path": [ - "reviewProduct" - ], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Furniture", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "upc" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name}}}" - } - } - ] - } - } - """ - -# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L136 -Scenario: multiple root mutations with correct service order - Given query - """ - mutation LoginAndReview( - $upc: String! - $body: String! - $updatedReview: UpdateReviewInput! - $username: String! - $password: String! - $reviewId: ID! - ) { - reviewProduct(upc: $upc, body: $body) { - ... on Furniture { - upc - } - } - updateReview(review: $updatedReview) { - id - body - } - login(username: $username, password: $password) { - reviews { - product { - upc - } - } - } - deleteReview(id: $reviewId) - } - """ - Then query plan - """ - { - "kind": "QueryPlan", - "node": { - "kind": "Sequence", - "nodes": [ - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [ - "upc", - "body", - "updatedReview" - ], - "operation": "mutation($upc:String!$body:String!$updatedReview:UpdateReviewInput!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{upc}}updateReview(review:$updatedReview){id body}}" - }, - { - "kind": "Fetch", - "serviceName": "accounts", - "variableUsages": [ - "username", - "password" - ], - "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" - }, - { - "kind": "Flatten", - "path": [ - "login" - ], - "node": { - "kind": "Fetch", - "serviceName": "reviews", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "User", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "id" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" - } - }, - { - "kind": "Flatten", - "path": [ - "login", - "reviews", - "@", - "product" - ], - "node": { - "kind": "Fetch", - "serviceName": "product", - "requires": [ - { - "kind": "InlineFragment", - "typeCondition": "Book", - "selections": [ - { - "kind": "Field", - "name": "__typename" - }, - { - "kind": "Field", - "name": "isbn" - } - ] - } - ], - "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" - } - }, - { - "kind": "Fetch", - "serviceName": "reviews", - "variableUsages": [ - "reviewId" - ], - "operation": "mutation($reviewId:ID!){deleteReview(id:$reviewId)}" - } - ] - } - } - """ diff --git a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts deleted file mode 100644 index 8e32e2c7581..00000000000 --- a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts +++ /dev/null @@ -1,1354 +0,0 @@ -import { GraphQLSchema, GraphQLError } from 'graphql'; -import gql from 'graphql-tag'; -import { buildQueryPlan, buildOperationContext } from '../buildQueryPlan'; -import { astSerializer, queryPlanSerializer } from '../snapshotSerializers'; -import { getFederatedTestingSchema } from './execution-utils'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -describe('buildQueryPlan', () => { - let schema: GraphQLSchema; - let errors: GraphQLError[]; - - beforeEach(() => { - ({ schema, errors } = getFederatedTestingSchema()); - expect(errors).toHaveLength(0); - }); - - it(`should not confuse union types with overlapping field names`, () => { - const query = gql` - query { - body { - ... on Image { - attributes { - url - } - } - ... on Text { - attributes { - bold - text - } - } - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "documents") { - { - body { - __typename - ... on Image { - attributes { - url - } - } - ... on Text { - attributes { - bold - text - } - } - } - } - }, - } - `); - }); - - it(`should use a single fetch when requesting a root field from one service`, () => { - const query = gql` - query { - me { - name - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "accounts") { - { - me { - name - } - } - }, - } - `); - }); - - it(`should use two independent fetches when requesting root fields from two services`, () => { - const query = gql` - query { - me { - name - } - topProducts { - name - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Parallel { - Fetch(service: "accounts") { - { - me { - name - } - } - }, - Sequence { - Fetch(service: "product") { - { - topProducts { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - name - } - } - } - }, - Flatten(path: "topProducts.@") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - __typename - isbn - title - year - } - } - }, - }, - Flatten(path: "topProducts.@") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - }, - } - `); - }); - - it(`should use a single fetch when requesting multiple root fields from the same service`, () => { - const query = gql` - query { - topProducts { - name - } - product(upc: "1") { - name - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - topProducts { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - name - } - } - product(upc: "1") { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - name - } - } - } - }, - Parallel { - Sequence { - Flatten(path: "topProducts.@") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - __typename - isbn - title - year - } - } - }, - }, - Flatten(path: "topProducts.@") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - Sequence { - Flatten(path: "product") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - __typename - isbn - title - year - } - } - }, - }, - Flatten(path: "product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - }, - }, - } - `); - }); - - it(`should use a single fetch when requesting relationship subfields from the same service`, () => { - const query = gql` - query { - topReviews { - body - author { - reviews { - body - } - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "reviews") { - { - topReviews { - body - author { - reviews { - body - } - } - } - } - }, - } - `); - }); - - it(`should use a single fetch when requesting relationship subfields and provided keys from the same service`, () => { - const query = gql` - query { - topReviews { - body - author { - id - reviews { - body - } - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "reviews") { - { - topReviews { - body - author { - id - reviews { - body - } - } - } - } - }, - } - `); - }); - - describe(`when requesting an extension field from another service`, () => { - it(`should add the field's representation requirements to the parent selection set and use a dependent fetch`, () => { - const query = gql` - query { - me { - name - reviews { - body - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "accounts") { - { - me { - name - __typename - id - } - } - }, - Flatten(path: "me") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - body - } - } - } - }, - }, - }, - } - `); - }); - - describe(`when the parent selection set is empty`, () => { - it(`should add the field's requirements to the parent selection set and use a dependent fetch`, () => { - const query = gql` - query { - me { - reviews { - body - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "accounts") { - { - me { - __typename - id - } - } - }, - Flatten(path: "me") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - body - } - } - } - }, - }, - }, - } - `); - }); - }); - - // TODO: Ask martijn about the meaning of this test - it(`should only add requirements once`, () => { - const query = gql` - query { - me { - reviews { - body - } - numberOfReviews - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "accounts") { - { - me { - __typename - id - } - } - }, - Flatten(path: "me") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - body - } - numberOfReviews - } - } - }, - }, - }, - } - `); - }); - }); - - describe(`when requesting a composite field with subfields from another service`, () => { - it(`should add key fields to the parent selection set and use a dependent fetch`, () => { - const query = gql` - query { - topReviews { - body - author { - name - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - topReviews { - body - author { - __typename - id - } - } - } - }, - Flatten(path: "topReviews.@.author") { - Fetch(service: "accounts") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - name - } - } - }, - }, - }, - } - `); - }); - - describe(`when requesting a field defined in another service which requires a field in the base service`, () => { - it(`should add the field provided by base service in first Fetch`, () => { - const query = gql` - query { - topCars { - retailPrice - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - topCars { - __typename - id - price - } - } - }, - Flatten(path: "topCars.@") { - Fetch(service: "reviews") { - { - ... on Car { - __typename - id - price - } - } => - { - ... on Car { - retailPrice - } - } - }, - }, - }, - } - `); - }); - }); - - describe(`when the parent selection set is empty`, () => { - it(`should add key fields to the parent selection set and use a dependent fetch`, () => { - const query = gql` - query { - topReviews { - author { - name - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - topReviews { - author { - __typename - id - } - } - } - }, - Flatten(path: "topReviews.@.author") { - Fetch(service: "accounts") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - name - } - } - }, - }, - }, - } - `); - }); - }); - }); - describe(`when requesting a relationship field with extension subfields from a different service`, () => { - it(`should first fetch the object using a key from the base service and then pass through the requirements`, () => { - const query = gql` - query { - topReviews { - author { - birthDate - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - topReviews { - author { - __typename - id - } - } - } - }, - Flatten(path: "topReviews.@.author") { - Fetch(service: "accounts") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - birthDate - } - } - }, - }, - }, - } - `); - }); - }); - - describe(`for abstract types`, () => { - // GraphQLError: Cannot query field "isbn" on type "Book" - // Probably an issue with extending / interfaces in composition. None of the fields from the base Book type - // are showing up in the resulting schema. - it(`should add __typename when fetching objects of an interface type from a service`, () => { - const query = gql` - query { - topProducts { - price - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "product") { - { - topProducts { - __typename - ... on Book { - price - } - ... on Furniture { - price - } - } - } - }, - } - `); - }); - }); - - // GraphQLError: Cannot query field "isbn" on type "Book" - // Probably an issue with extending / interfaces in composition. None of the fields from the base Book type - // are showing up in the resulting schema. - it(`should break up when traversing an extension field on an interface type from a service`, () => { - const query = gql` - query { - topProducts { - price - reviews { - body - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - topProducts { - __typename - ... on Book { - price - __typename - isbn - } - ... on Furniture { - price - __typename - upc - } - } - } - }, - Flatten(path: "topProducts.@") { - Fetch(service: "reviews") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } => - { - ... on Book { - reviews { - body - } - } - ... on Furniture { - reviews { - body - } - } - } - }, - }, - }, - } - `); - }); - - it(`interface fragments should expand into possible types only`, () => { - const query = gql` - query { - books { - ... on Product { - name - ... on Furniture { - upc - } - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "books") { - { - books { - __typename - isbn - title - year - } - } - }, - Flatten(path: "books.@") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - } - `); - }); - - it(`interface inside interface should expand into possible types only`, () => { - const query = gql` - query { - product(upc: "") { - details { - country - } - } - } - `; - - const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "product") { - { - product(upc: "") { - __typename - ... on Book { - details { - country - } - } - ... on Furniture { - details { - country - } - } - } - } - }, - } - `); - }); - - describe(`experimental compression to downstream services`, () => { - it(`should generate fragments internally to downstream requests`, () => { - const query = gql` - query { - topReviews { - body - author - product { - name - price - details { - country - } - } - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - { autoFragmentization: true }, - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - topReviews { - ...__QueryPlanFragment_1__ - } - } - fragment __QueryPlanFragment_1__ on Review { - body - author - product { - ...__QueryPlanFragment_0__ - } - } - fragment __QueryPlanFragment_0__ on Product { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } - }, - Parallel { - Sequence { - Flatten(path: "topReviews.@.product") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - __typename - isbn - title - year - } - } - }, - }, - Flatten(path: "topReviews.@.product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - Flatten(path: "topReviews.@.product") { - Fetch(service: "product") { - { - ... on Furniture { - __typename - upc - } - ... on Book { - __typename - isbn - } - } => - { - ... on Furniture { - name - price - details { - country - } - } - ... on Book { - price - details { - country - } - } - } - }, - }, - }, - }, - } - `); - }); - - it(`shouldn't generate fragments for selection sets of length 2 or less`, () => { - const query = gql` - query { - topReviews { - body - author - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - { autoFragmentization: true }, - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "reviews") { - { - topReviews { - body - author - } - } - }, - } - `); - }); - - it(`should generate fragments for selection sets of length 3 or greater`, () => { - const query = gql` - query { - topReviews { - id - body - author - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - { autoFragmentization: true }, - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "reviews") { - { - topReviews { - ...__QueryPlanFragment_0__ - } - } - fragment __QueryPlanFragment_0__ on Review { - id - body - author - } - }, - } - `); - }); - - it(`should generate fragments correctly when aliases are used`, () => { - const query = gql` - query { - reviews: topReviews { - content: body - author - product { - name - cost: price - details { - origin: country - } - } - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - { autoFragmentization: true }, - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - reviews: topReviews { - ...__QueryPlanFragment_1__ - } - } - fragment __QueryPlanFragment_1__ on Review { - content: body - author - product { - ...__QueryPlanFragment_0__ - } - } - fragment __QueryPlanFragment_0__ on Product { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } - }, - Parallel { - Sequence { - Flatten(path: "reviews.@.product") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - __typename - isbn - title - year - } - } - }, - }, - Flatten(path: "reviews.@.product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - Flatten(path: "reviews.@.product") { - Fetch(service: "product") { - { - ... on Furniture { - __typename - upc - } - ... on Book { - __typename - isbn - } - } => - { - ... on Furniture { - name - cost: price - details { - origin: country - } - } - ... on Book { - cost: price - details { - origin: country - } - } - } - }, - }, - }, - }, - } - `); - }); - }); - - it(`should properly expand nested unions with inline fragments`, () => { - const query = gql` - query { - body { - ... on Image { - ... on Body { - ... on Image { - attributes { - url - } - } - ... on Text { - attributes { - bold - text - } - } - } - } - ... on Text { - attributes { - bold - } - } - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "documents") { - { - body { - __typename - ... on Image { - attributes { - url - } - } - ... on Text { - attributes { - bold - } - } - } - } - }, - } - `); - }); - - describe('deduplicates fields / selections regardless of adjacency and type condition nesting', () => { - it('for inline fragments', () => { - const query = gql` - query { - body { - ... on Image { - ... on Text { - attributes { - bold - } - } - } - ... on Body { - ... on Text { - attributes { - bold - text - } - } - } - ... on Text { - attributes { - bold - text - } - } - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "documents") { - { - body { - __typename - ... on Text { - attributes { - bold - text - } - } - } - } - }, - } - `); - }); - - it('for named fragment spreads', () => { - const query = gql` - fragment TextFragment on Text { - attributes { - bold - text - } - } - - query { - body { - ... on Image { - ...TextFragment - } - ... on Body { - ...TextFragment - } - ...TextFragment - } - } - `; - - const queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - ); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "documents") { - { - body { - __typename - ... on Text { - attributes { - bold - text - } - } - } - } - }, - } - `); - }); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts deleted file mode 100644 index a8041520fbf..00000000000 --- a/packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts +++ /dev/null @@ -1,724 +0,0 @@ -import { GraphQLSchema, GraphQLError, getIntrospectionQuery } from 'graphql'; -import { addResolversToSchema, GraphQLResolverMap } from 'apollo-graphql'; -import gql from 'graphql-tag'; -import { GraphQLRequestContext } from 'apollo-server-types'; -import { AuthenticationError } from 'apollo-server-core'; - -import { buildQueryPlan, buildOperationContext } from '../buildQueryPlan'; -import { executeQueryPlan } from '../executeQueryPlan'; -import { LocalGraphQLDataSource } from '../datasources/LocalGraphQLDataSource'; - -import { astSerializer, queryPlanSerializer } from '../snapshotSerializers'; -import { getFederatedTestingSchema } from './execution-utils'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -describe('executeQueryPlan', () => { - let serviceMap: { - [serviceName: string]: LocalGraphQLDataSource; - }; - - function overrideResolversInService( - serviceName: string, - resolvers: GraphQLResolverMap, - ) { - addResolversToSchema(serviceMap[serviceName].schema, resolvers); - } - - let schema: GraphQLSchema; - let errors: GraphQLError[]; - - beforeEach(() => { - ({ serviceMap, schema, errors } = getFederatedTestingSchema()); - expect(errors).toHaveLength(0); - }); - - function buildRequestContext(): GraphQLRequestContext { - return { - cache: undefined as any, - context: {}, - request: { - variables: {}, - }, - } as GraphQLRequestContext; - } - - describe(`errors`, () => { - it(`should not include an empty "errors" array when no errors were encountered`, async () => { - const query = gql` - query { - me { - name { - first - last - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response).not.toHaveProperty('errors'); - }); - - it(`should include an error when a root-level field errors out`, async () => { - overrideResolversInService('accounts', { - RootQuery: { - me() { - throw new AuthenticationError('Something went wrong'); - }, - }, - }); - - const query = gql` - query { - me { - name { - first - last - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response).toHaveProperty('data.me', null); - expect(response).toHaveProperty( - 'errors.0.message', - 'Something went wrong', - ); - expect(response).toHaveProperty( - 'errors.0.extensions.code', - 'UNAUTHENTICATED', - ); - expect(response).toHaveProperty( - 'errors.0.extensions.serviceName', - 'accounts', - ); - expect(response).toHaveProperty( - 'errors.0.extensions.query', - '{me{name{first last}}}', - ); - expect(response).toHaveProperty('errors.0.extensions.variables', {}); - }); - - it(`should still include other root-level results if one root-level field errors out`, async () => { - overrideResolversInService('accounts', { - RootQuery: { - me() { - throw new Error('Something went wrong'); - }, - }, - }); - - const query = gql` - query { - me { - name { - first - last - } - } - topReviews { - body - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response).toHaveProperty('data.me', null); - expect(response).toHaveProperty('data.topReviews', expect.any(Array)); - }); - - it(`should still include data from other services if one services is unavailable`, async () => { - delete serviceMap.accounts; - - const query = gql` - query { - me { - name { - first - last - } - } - topReviews { - body - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response).toHaveProperty('data.me', null); - expect(response).toHaveProperty('data.topReviews', expect.any(Array)); - }); - }); - - it(`should only return fields that have been requested directly`, async () => { - const query = gql` - query { - topReviews { - body - author { - name { - first - last - } - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "topReviews": Array [ - Object { - "author": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Love it!", - }, - Object { - "author": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Too expensive.", - }, - Object { - "author": Object { - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Could be better.", - }, - Object { - "author": Object { - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Prefer something else.", - }, - Object { - "author": Object { - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Wish I had read this before.", - }, - ], - } - `); - }); - - it('should not duplicate variable definitions', async () => { - const query = gql` - query Test($first: Int!) { - first: topReviews(first: $first) { - body - author { - name { - first - last - } - } - } - second: topReviews(first: $first) { - body - author { - name { - first - last - } - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const requestContext = buildRequestContext(); - requestContext.request.variables = { first: 3 }; - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - requestContext, - operationContext, - ); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "first": Array [ - Object { - "author": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Love it!", - }, - Object { - "author": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Too expensive.", - }, - Object { - "author": Object { - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Could be better.", - }, - ], - "second": Array [ - Object { - "author": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Love it!", - }, - Object { - "author": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Too expensive.", - }, - Object { - "author": Object { - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Could be better.", - }, - ], - } - `); - }); - - it('should include variables in non-root requests', async () => { - const query = gql` - query Test($locale: String) { - topReviews { - body - author { - name { - first - last - } - birthDate(locale: $locale) - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const requestContext = buildRequestContext(); - requestContext.request.variables = { locale: 'en-US' }; - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - requestContext, - operationContext, - ); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "topReviews": Array [ - Object { - "author": Object { - "birthDate": "12/10/1815", - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Love it!", - }, - Object { - "author": Object { - "birthDate": "12/10/1815", - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "body": "Too expensive.", - }, - Object { - "author": Object { - "birthDate": "6/23/1912", - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Could be better.", - }, - Object { - "author": Object { - "birthDate": "6/23/1912", - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Prefer something else.", - }, - Object { - "author": Object { - "birthDate": "6/23/1912", - "name": Object { - "first": "Alan", - "last": "Turing", - }, - }, - "body": "Wish I had read this before.", - }, - ], - } - `); - }); - - it('can execute an introspection query', async () => { - const operationContext = buildOperationContext( - schema, - gql` - ${getIntrospectionQuery()} - `, - ); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.data).toHaveProperty('__schema'); - expect(response.errors).toBeUndefined(); - }); - - it(`can execute queries on interface types`, async () => { - const query = gql` - query { - vehicle(id: "1") { - description - price - retailPrice - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "vehicle": Object { - "description": "Humble Toyota", - "price": "9990", - "retailPrice": "9990", - }, - } - `); - }); - - it(`can execute queries whose fields are interface types`, async () => { - const query = gql` - query { - user(id: "1") { - name { - first - last - } - vehicle { - description - price - retailPrice - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "user": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - "vehicle": Object { - "description": "Humble Toyota", - "price": "9990", - "retailPrice": "9990", - }, - }, - } - `); - }); - - it(`can execute queries whose fields are union types`, async () => { - const query = gql` - query { - user(id: "1") { - name { - first - last - } - thing { - ... on Vehicle { - description - price - retailPrice - } - ... on Ikea { - asile - } - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "user": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - "thing": Object { - "description": "Humble Toyota", - "price": "9990", - "retailPrice": "9990", - }, - }, - } - `); - }); - - it('can execute queries with falsey @requires (except undefined)', async () => { - const query = gql` - query { - books { - name # Requires title, year (on Book type) - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "books": Array [ - Object { - "name": "Structure and Interpretation of Computer Programs (1996)", - }, - Object { - "name": "Object Oriented Software Construction (1997)", - }, - Object { - "name": "Design Patterns (1995)", - }, - Object { - "name": "The Year Was Null (null)", - }, - Object { - "name": " (404)", - }, - Object { - "name": "No Books Like This Book! (2019)", - }, - ], - } - `); - }); - - it('can execute queries with list @requires', async () => { - const query = gql` - query { - book(isbn: "0201633612") { - # Requires similarBooks { isbn } - relatedReviews { - id - body - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.errors).toMatchInlineSnapshot(`undefined`); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "book": Object { - "relatedReviews": Array [ - Object { - "body": "A classic.", - "id": "6", - }, - Object { - "body": "A bit outdated.", - "id": "5", - }, - ], - }, - } - `); - }); - - it('can execute queries with selections on null @requires fields', async () => { - const query = gql` - query { - book(isbn: "0987654321") { - # Requires similarBooks { isbn } - relatedReviews { - id - body - } - } - } - `; - - const operationContext = buildOperationContext(schema, query); - const queryPlan = buildQueryPlan(operationContext); - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - buildRequestContext(), - operationContext, - ); - - expect(response.errors).toBeUndefined(); - - expect(response.data).toMatchInlineSnapshot(` - Object { - "book": Object { - "relatedReviews": Array [], - }, - } - `); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/execution-utils.ts b/packages/apollo-gateway/src/__tests__/execution-utils.ts deleted file mode 100644 index 30b86a23dea..00000000000 --- a/packages/apollo-gateway/src/__tests__/execution-utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - GraphQLSchemaValidationError, - GraphQLSchemaModule, - GraphQLResolverMap, -} from 'apollo-graphql'; -import { GraphQLRequest, GraphQLExecutionResult, Logger } from 'apollo-server-types'; -import { - composeAndValidate, - buildFederatedSchema, - ServiceDefinition, -} from '@apollo/federation'; - -import { - buildQueryPlan, - executeQueryPlan, - QueryPlan, - buildOperationContext, -} from '@apollo/gateway'; -import { LocalGraphQLDataSource } from '../datasources/LocalGraphQLDataSource'; -import { mergeDeep } from 'apollo-utilities'; - -import queryPlanSerializer from '../snapshotSerializers/queryPlanSerializer'; -import astSerializer from '../snapshotSerializers/astSerializer'; -import gql from 'graphql-tag'; -import { fixtures } from 'apollo-federation-integration-testsuite'; - -const prettyFormat = require('pretty-format'); - -export type ServiceDefinitionModule = ServiceDefinition & GraphQLSchemaModule; - -export function overrideResolversInService( - module: ServiceDefinitionModule, - resolvers: GraphQLResolverMap, -): ServiceDefinitionModule { - return { - name: module.name, - typeDefs: module.typeDefs, - resolvers: mergeDeep(module.resolvers, resolvers), - }; -} - -export async function execute( - request: GraphQLRequest, - services: ServiceDefinitionModule[] = fixtures, - logger: Logger = console, -): Promise { - const serviceMap = Object.fromEntries( - services.map(({ name, typeDefs, resolvers }) => { - return [ - name, - new LocalGraphQLDataSource( - buildFederatedSchema([{ typeDefs, resolvers }]), - ), - ] as [string, LocalGraphQLDataSource]; - }), - ); - - const { errors, schema } = getFederatedTestingSchema(services); - - if (errors && errors.length > 0) { - throw new GraphQLSchemaValidationError(errors); - } - const operationContext = buildOperationContext(schema, gql`${request.query}`); - - const queryPlan = buildQueryPlan(operationContext); - - const result = await executeQueryPlan( - queryPlan, - serviceMap, - { - cache: undefined as any, - context: {}, - request, - logger - }, - operationContext, - ); - - return { ...result, queryPlan }; -} - -export function buildLocalService(modules: GraphQLSchemaModule[]) { - const schema = buildFederatedSchema(modules); - return new LocalGraphQLDataSource(schema); -} - -export function getFederatedTestingSchema(services: ServiceDefinitionModule[] = fixtures) { - const serviceMap = Object.fromEntries( - services.map((service) => [ - service.name, - buildLocalService([service]), - ]), - ); - - const { schema, errors } = composeAndValidate( - Object.entries(serviceMap).map(([serviceName, dataSource]) => ({ - name: serviceName, - typeDefs: dataSource.sdl(), - })), - ); - - return { serviceMap, schema, errors }; -} - -export function wait(ms: number) { - return new Promise(r => setTimeout(r, ms)); -} - -export function printPlan(queryPlan: QueryPlan): string { - return prettyFormat(queryPlan, { - plugins: [queryPlanSerializer, astSerializer], - }); -} diff --git a/packages/apollo-gateway/src/__tests__/gateway/buildService.test.ts b/packages/apollo-gateway/src/__tests__/gateway/buildService.test.ts deleted file mode 100644 index 1f08046bb33..00000000000 --- a/packages/apollo-gateway/src/__tests__/gateway/buildService.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import gql from 'graphql-tag'; -import { fetch } from '__mocks__/apollo-server-env'; -import { createTestClient } from 'apollo-server-testing'; -import { ApolloServerBase as ApolloServer } from 'apollo-server-core'; - -import { RemoteGraphQLDataSource } from '../../datasources/RemoteGraphQLDataSource'; -import { ApolloGateway, SERVICE_DEFINITION_QUERY } from '../../'; -import { fixtures } from 'apollo-federation-integration-testsuite'; - -beforeEach(() => { - fetch.mockReset(); -}); - -it('calls buildService only once per service', async () => { - fetch.mockJSONResponseOnce({ - data: { _service: { sdl: `extend type Query { thing: String }` } }, - }); - - const buildServiceSpy = jest.fn(() => { - return new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - }); - }); - - const gateway = new ApolloGateway({ - serviceList: [{ name: 'foo', url: 'https://api.example.com/foo' }], - buildService: buildServiceSpy - }); - - await gateway.load(); - - expect(buildServiceSpy).toHaveBeenCalledTimes(1); -}); - -it('correctly passes the context from ApolloServer to datasources', async () => { - const gateway = new ApolloGateway({ - localServiceList: fixtures, - buildService: _service => { - return new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - willSendRequest: ({ request, context }) => { - request.http?.headers.set('x-user-id', context.userId); - }, - }); - }, - }); - - const { schema, executor } = await gateway.load(); - - const server = new ApolloServer({ - schema, - executor, - context: () => ({ - userId: '1234', - }), - }); - - const call = createTestClient(server); - - const query = gql` - { - me { - username - } - } - `; - - fetch.mockJSONResponseOnce({ data: { me: { username: '@jbaxleyiii' } } }); - - const result = await call.query({ - query, - }); - - expect(result.errors).toBeUndefined(); - expect(result.data).toEqual({ - me: { username: '@jbaxleyiii' }, - }); - - expect(fetch).toBeCalledTimes(1); - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/foo', - body: { - query: `{me{username}}`, - variables: {}, - }, - headers: { - 'x-user-id': '1234', - }, - }); -}); - -function createSdlData(sdl: string): object { - return { - data: { - _service: { - sdl: sdl, - }, - }, - }; -} - -it('makes enhanced introspection request using datasource', async () => { - fetch.mockJSONResponseOnce( - createSdlData('extend type Query { one: String }'), - ); - - const gateway = new ApolloGateway({ - serviceList: [ - { - name: 'one', - url: 'https://api.example.com/one', - }, - ], - buildService: _service => { - return new RemoteGraphQLDataSource({ - url: 'https://api.example.com/override', - willSendRequest: ({ request }) => { - request.http?.headers.set('custom-header', 'some-custom-value'); - }, - }); - }, - }); - - await gateway.load(); - - expect(fetch).toBeCalledTimes(1); - - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/override', - body: { - query: SERVICE_DEFINITION_QUERY, - }, - headers: { - 'custom-header': 'some-custom-value', - }, - }); -}); - -it('customizes request on a per-service basis', async () => { - fetch - .mockJSONResponseOnce(createSdlData('extend type Query { one: String }')) - .mockJSONResponseOnce(createSdlData('extend type Query { two: String }')) - .mockJSONResponseOnce(createSdlData('extend type Query { three: String }')); - - const gateway = new ApolloGateway({ - serviceList: [ - { - name: 'one', - url: 'https://api.example.com/one', - }, - { - name: 'two', - url: 'https://api.example.com/two', - }, - { - name: 'three', - url: 'https://api.example.com/three', - }, - ], - buildService: service => { - return new RemoteGraphQLDataSource({ - url: service.url, - willSendRequest: ({ request }) => { - request.http?.headers.set('service-name', service.name); - }, - }); - }, - }); - - await gateway.load(); - - expect(fetch).toBeCalledTimes(3); - - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/one', - body: { - query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`, - }, - headers: { - 'service-name': 'one', - }, - }); - - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/two', - body: { - query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`, - }, - headers: { - 'service-name': 'two', - }, - }); - - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/three', - body: { - query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`, - }, - headers: { - 'service-name': 'three', - }, - }); -}); - -it('does not share service definition cache between gateways', async () => { - let updates = 0; - const updateObserver: any = (..._args: any[]) => { - updates += 1; - }; - - // Initialize first gateway - { - fetch.mockJSONResponseOnce( - createSdlData('extend type Query { repeat: String }'), - ); - - const gateway = new ApolloGateway({ - serviceList: [ - { - name: 'repeat', - url: 'https://api.example.com/repeat', - }, - ], - experimental_didUpdateComposition: updateObserver, - }); - - await gateway.load(); - } - - // Initialize second gateway - { - fetch.mockJSONResponseOnce( - createSdlData('extend type Query { repeat: String }'), - ); - - const gateway = new ApolloGateway({ - serviceList: [ - { - name: 'repeat', - url: 'https://api.example.com/repeat', - }, - ], - experimental_didUpdateComposition: updateObserver, - }); - - await gateway.load(); - } - - expect(updates).toEqual(2); -}); diff --git a/packages/apollo-gateway/src/__tests__/gateway/executor.test.ts b/packages/apollo-gateway/src/__tests__/gateway/executor.test.ts deleted file mode 100644 index 51cf8fdcd8a..00000000000 --- a/packages/apollo-gateway/src/__tests__/gateway/executor.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import gql from 'graphql-tag'; -import { ApolloGateway } from '../../'; -import { ApolloServer } from "apollo-server"; -import { fixtures } from 'apollo-federation-integration-testsuite'; -import { Logger } from 'apollo-server-types'; - -let logger: Logger; - -beforeEach(() => { - const warn = jest.fn(); - const debug = jest.fn(); - const error = jest.fn(); - const info = jest.fn(); - - logger = { - warn, - debug, - error, - info, - }; -}); - -describe('ApolloGateway executor', () => { - it('validates requests prior to execution', async () => { - const gateway = new ApolloGateway({ - localServiceList: fixtures, - }); - - const { executor } = await gateway.load(); - - const { errors } = await executor({ - document: gql` - query InvalidVariables($first: Int!) { - topReviews(first: $first) { - body - } - } - `, - request: { - variables: { first: '3' }, - }, - queryHash: 'hashed', - context: null, - cache: {} as any, - logger, - }); - - expect(errors![0].message).toMatch( - 'Variable "$first" got invalid value "3";', - ); - }); - - it('still sets the ApolloServer executor on load rejection', async () => { - const gateway = new ApolloGateway({ - // Empty service list will trigger the gateway to crash on load, which is what we want. - serviceList: [], - logger, - }); - - // Mock implementation of process.exit with another () => never function. - // This is because the gateway doesn't just throw in this scenario, it crashes. - const mockExit = jest - .spyOn(process, 'exit') - .mockImplementation((code) => { - throw new Error(code?.toString()); - }); - - const server = new ApolloServer({ - gateway, - subscriptions: false, - logger, - }); - - // Ensure the throw happens to maintain the correctness of this test. - await expect( - server.executeOperation({ query: '{ __typename }' })).rejects.toThrow(); - - expect(server.requestOptions.executor).toBe(gateway.executor); - - expect(logger.error.mock.calls).toEqual([ - ["Error checking for changes to service definitions: Tried to load services from remote endpoints but none provided"], - ["This data graph is missing a valid configuration. Tried to load services from remote endpoints but none provided"] - ]); - - mockExit.mockRestore(); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/gateway/lifecycle-hooks.test.ts b/packages/apollo-gateway/src/__tests__/gateway/lifecycle-hooks.test.ts deleted file mode 100644 index 8c4eaa593b6..00000000000 --- a/packages/apollo-gateway/src/__tests__/gateway/lifecycle-hooks.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -import gql from 'graphql-tag'; -import { - ApolloGateway, - GatewayConfig, - Experimental_DidResolveQueryPlanCallback, - Experimental_UpdateServiceDefinitions, -} from '../../index'; -import { - product, - reviews, - inventory, - accounts, - books, - documents, -} from 'apollo-federation-integration-testsuite'; -import { Logger } from 'apollo-server-types'; - -// The order of this was specified to preserve existing test coverage. Typically -// we would just import and use the `fixtures` array. -const serviceDefinitions = [ - product, - reviews, - inventory, - accounts, - books, - documents, -].map((s, i) => ({ - name: s.name, - typeDefs: s.typeDefs, - url: `http://localhost:${i}`, -})); - -let logger: Logger; - -beforeEach(() => { - const warn = jest.fn(); - const debug = jest.fn(); - const error = jest.fn(); - const info = jest.fn(); - - logger = { - warn, - debug, - error, - info, - }; -}); - -describe('lifecycle hooks', () => { - it('uses updateServiceDefinitions override', async () => { - const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = jest.fn( - async (_config: GatewayConfig) => { - return { serviceDefinitions, isNewSchema: true }; - }, - ); - - const gateway = new ApolloGateway({ - serviceList: serviceDefinitions, - experimental_updateServiceDefinitions, - experimental_didUpdateComposition: jest.fn(), - logger, - }); - - await gateway.load(); - - expect(experimental_updateServiceDefinitions).toBeCalled(); - expect(gateway.schema!.getType('Furniture')).toBeDefined(); - }); - - it('calls experimental_didFailComposition with a bad config', async () => { - const experimental_didFailComposition = jest.fn(); - - const gateway = new ApolloGateway({ - async experimental_updateServiceDefinitions(_config: GatewayConfig) { - return { - serviceDefinitions: [serviceDefinitions[0]], - compositionMetadata: { - formatVersion: 1, - id: 'abc', - implementingServiceLocations: [], - schemaHash: 'abc', - }, - isNewSchema: true, - }; - }, - serviceList: [], - experimental_didFailComposition, - logger, - }); - - await expect(gateway.load()).rejects.toThrowError(); - - const callbackArgs = experimental_didFailComposition.mock.calls[0][0]; - expect(callbackArgs.serviceList).toHaveLength(1); - expect(callbackArgs.errors[0]).toMatchInlineSnapshot( - `[GraphQLError: [product] Book -> \`Book\` is an extension type, but \`Book\` is not defined in any service]`, - ); - expect(callbackArgs.compositionMetadata.id).toEqual('abc'); - expect(experimental_didFailComposition).toBeCalled(); - }); - - it('calls experimental_didUpdateComposition on schema update', async () => { - const compositionMetadata = { - formatVersion: 1, - id: 'abc', - implementingServiceLocations: [], - schemaHash: 'hash1', - }; - - const update: Experimental_UpdateServiceDefinitions = async ( - _config: GatewayConfig, - ) => ({ - serviceDefinitions, - isNewSchema: true, - compositionMetadata: { - ...compositionMetadata, - id: '123', - schemaHash: 'hash2', - }, - }); - - // This is the simplest way I could find to achieve mocked functions that leverage our types - const mockUpdate = jest.fn(update); - - // We want to return a different composition across two ticks, so we mock it - // slightly differenty - mockUpdate.mockImplementationOnce(async (_config: GatewayConfig) => { - const services = serviceDefinitions.filter(s => s.name !== 'books'); - return { - serviceDefinitions: [ - ...services, - { - name: 'book', - typeDefs: books.typeDefs, - url: 'http://localhost:32542', - }, - ], - isNewSchema: true, - compositionMetadata, - }; - }); - - const mockDidUpdate = jest.fn(); - - const gateway = new ApolloGateway({ - experimental_updateServiceDefinitions: mockUpdate, - experimental_didUpdateComposition: mockDidUpdate, - logger, - }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; - - let resolve1: Function; - let resolve2: Function; - const schemaChangeBlocker1 = new Promise(res => (resolve1 = res)); - const schemaChangeBlocker2 = new Promise(res => (resolve2 = res)); - - gateway.onSchemaChange( - jest - .fn() - .mockImplementationOnce(() => resolve1()) - .mockImplementationOnce(() => resolve2()), - ); - - await gateway.load(); - - await schemaChangeBlocker1; - expect(mockUpdate).toBeCalledTimes(1); - expect(mockDidUpdate).toBeCalledTimes(1); - - await schemaChangeBlocker2; - expect(mockUpdate).toBeCalledTimes(2); - expect(mockDidUpdate).toBeCalledTimes(2); - - const [firstCall, secondCall] = mockDidUpdate.mock.calls; - - expect(firstCall[0]!.schema).toBeDefined(); - expect(firstCall[0].compositionMetadata!.schemaHash).toEqual('hash1'); - // first call should have no second "previous" argument - expect(firstCall[1]).toBeUndefined(); - - expect(secondCall[0].schema).toBeDefined(); - expect(secondCall[0].compositionMetadata!.schemaHash).toEqual('hash2'); - // second call should have previous info in the second arg - expect(secondCall[1]!.schema).toBeDefined(); - expect(secondCall[1]!.compositionMetadata!.schemaHash).toEqual('hash1'); - }); - - it('uses default service definition updater', async () => { - const gateway = new ApolloGateway({ - localServiceList: serviceDefinitions, - logger, - }); - - const { schema } = await gateway.load(); - - // spying on gateway.loadServiceDefinitions wasn't working, so this also - // should test functionality. If there's no overwriting service definition - // updater, it has to use the default. If there's a valid schema, then - // the loader had to have been called. - expect(schema.getType('User')).toBeDefined(); - }); - - it('warns when polling on the default fetcher', async () => { - new ApolloGateway({ - serviceList: serviceDefinitions, - experimental_pollInterval: 10, - logger, - }); - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn).toHaveBeenCalledWith( - 'Polling running services is dangerous and not recommended in production. Polling should only be used against a registry. If you are polling running services, use with caution.', - ); - }); - - it('registers schema change callbacks when experimental_pollInterval is set for unmanaged configs', async () => { - const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = jest.fn( - async (_config: GatewayConfig) => { - return { serviceDefinitions, isNewSchema: true }; - }, - ); - - const gateway = new ApolloGateway({ - serviceList: [{ name: 'book', url: 'http://localhost:32542' }], - experimental_updateServiceDefinitions, - experimental_pollInterval: 100, - logger, - }); - - let resolve: Function; - const schemaChangeBlocker = new Promise(res => (resolve = res)); - const schemaChangeCallback = jest.fn(() => resolve()); - - gateway.onSchemaChange(schemaChangeCallback); - gateway.load(); - - await schemaChangeBlocker; - - expect(schemaChangeCallback).toBeCalledTimes(1); - }); - - it('calls experimental_didResolveQueryPlan when executor is called', async () => { - const experimental_didResolveQueryPlan: Experimental_DidResolveQueryPlanCallback = jest.fn() - - const gateway = new ApolloGateway({ - localServiceList: [ - books - ], - experimental_didResolveQueryPlan, - }); - - const { executor } = await gateway.load(); - await executor({ - document: gql` - { book(isbn: "0262510871") { year } } - `, - request: {}, - queryHash: 'hashed', - context: {}, - }); - - expect(experimental_didResolveQueryPlan).toBeCalled(); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/gateway/queryPlanCache.test.ts b/packages/apollo-gateway/src/__tests__/gateway/queryPlanCache.test.ts deleted file mode 100644 index f1deddf6584..00000000000 --- a/packages/apollo-gateway/src/__tests__/gateway/queryPlanCache.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import gql from 'graphql-tag'; -import { createTestClient } from 'apollo-server-testing'; -import { ApolloServerBase as ApolloServer } from 'apollo-server-core'; -import { buildFederatedSchema } from '@apollo/federation'; - -import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource'; -import { ApolloGateway } from '../../'; -import { fixtures } from 'apollo-federation-integration-testsuite'; - -it('caches the query plan for a request', async () => { - const planner = require('../../buildQueryPlan'); - const originalPlanner = planner.buildQueryPlan; - - planner.buildQueryPlan = jest.fn(originalPlanner); - - const gateway = new ApolloGateway({ - localServiceList: fixtures, - buildService: service => { - return new LocalGraphQLDataSource(buildFederatedSchema([service])); - }, - }); - - const { schema, executor } = await gateway.load(); - - const server = new ApolloServer({ schema, executor }); - - const upc = '1'; - const call = createTestClient(server); - - const query = gql` - query GetProduct($upc: String!) { - product(upc: $upc) { - name - } - } - `; - - const result = await call.query({ - query, - variables: { upc }, - }); - - expect(result.data).toEqual({ - product: { - name: 'Table', - }, - }); - - const secondResult = await call.query({ - query, - variables: { upc }, - }); - - expect(result.data).toEqual(secondResult.data); - expect(planner.buildQueryPlan).toHaveBeenCalledTimes(1); -}); - -it('supports multiple operations and operationName', async () => { - const query = `#graphql - query GetUser { - me { - username - } - } - query GetReviews { - topReviews { - body - } - } - `; - - const gateway = new ApolloGateway({ - localServiceList: fixtures, - buildService: service => { - return new LocalGraphQLDataSource(buildFederatedSchema([service])); - }, - }); - - const { schema, executor } = await gateway.load(); - - const server = new ApolloServer({ schema, executor }); - - const { data: userData } = await server.executeOperation({ - query, - operationName: 'GetUser', - }); - - const { data: reviewsData } = await server.executeOperation({ - query, - operationName: 'GetReviews', - }); - - expect(userData).toEqual({ - me: { username: '@ada' }, - }); - expect(reviewsData).toEqual({ - topReviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'Could be better.' }, - { body: 'Prefer something else.' }, - { body: 'Wish I had read this before.' }, - ], - }); -}); - -it('does not corrupt cached queryplan data across requests', async () => { - const serviceA = { - name: 'a', - typeDefs: gql` - type Query { - user: User - } - - type User @key(fields: "id") { - id: ID! - preferences: Preferences - } - - type Preferences { - favorites: Things - } - - type Things { - color: String - animal: String - } - `, - resolvers: { - Query: { - user() { - return { - id: '1', - preferences: { - favorites: { color: 'limegreen', animal: 'platypus' }, - }, - }; - }, - }, - }, - }; - - const serviceB = { - name: 'b', - typeDefs: gql` - extend type User @key(fields: "id") { - id: ID! @external - preferences: Preferences @external - favoriteColor: String - @requires(fields: "preferences { favorites { color } }") - favoriteAnimal: String - @requires(fields: "preferences { favorites { animal } }") - } - - extend type Preferences { - favorites: Things @external - } - - extend type Things { - color: String @external - animal: String @external - } - `, - resolvers: { - User: { - favoriteColor(user: any) { - return user.preferences.favorites.color; - }, - favoriteAnimal(user: any) { - return user.preferences.favorites.animal; - }, - }, - }, - }; - - const gateway = new ApolloGateway({ - localServiceList: [serviceA, serviceB], - buildService: service => { - return new LocalGraphQLDataSource(buildFederatedSchema([service])); - }, - }); - - const { schema, executor } = await gateway.load(); - - const server = new ApolloServer({ schema, executor }); - - const call = createTestClient(server); - - const query1 = `#graphql - query UserFavoriteColor { - user { - favoriteColor - } - } - `; - - const query2 = `#graphql - query UserFavorites { - user { - favoriteColor - favoriteAnimal - } - } - `; - - const result1 = await call.query({ - query: query1, - }); - const result2 = await call.query({ - query: query2, - }); - const result3 = await call.query({ - query: query1, - }); - - expect(result1.errors).toEqual(undefined); - expect(result2.errors).toEqual(undefined); - expect(result3.errors).toEqual(undefined); - expect(result1).toEqual(result3); -}); diff --git a/packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts b/packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts deleted file mode 100644 index 25f83b2c7a6..00000000000 --- a/packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts +++ /dev/null @@ -1,605 +0,0 @@ -import { gunzipSync } from 'zlib'; -import nock from 'nock'; -import { GraphQLSchemaModule } from 'apollo-graphql'; -import gql from 'graphql-tag'; -import { buildFederatedSchema } from '@apollo/federation'; -import { ApolloServer } from 'apollo-server'; -import { execute, toPromise } from 'apollo-link'; -import { createHttpLink } from 'apollo-link-http'; -import fetch from 'node-fetch'; -import { ApolloGateway } from '../..'; -import { Plugin, Config, Refs } from 'pretty-format'; -import { Report } from 'apollo-engine-reporting-protobuf'; -import { fixtures } from 'apollo-federation-integration-testsuite'; - -// Normalize specific fields that change often (eg timestamps) to static values, -// to make snapshot testing viable. (If these helpers are more generally -// useful, they could be moved to a different file.) - -const alreadyProcessed = '__already_processed__'; - -function replaceFieldValuesSerializer( - replacements: Record, -): Plugin { - const fieldNames = Object.keys(replacements); - return { - test(value: any) { - return ( - value && - typeof value === 'object' && - !value[alreadyProcessed] && - fieldNames.some((n) => n in value) - ); - }, - - serialize( - value: Record, - config: Config, - indentation: string, - depth: number, - refs: Refs, - printer: any, - ): string { - // Clone object so pretty-format doesn't consider it as a circular - // reference. Put a special (non-enumerable) property on it so that *we* - // don't reprocess it ourselves. - const newValue = { ...value }; - Object.defineProperty(newValue, alreadyProcessed, { value: true }); - fieldNames.forEach((fn) => { - if (fn in value) { - const replacement = replacements[fn]; - if (typeof replacement === 'function') { - newValue[fn] = replacement(value[fn]); - } else { - newValue[fn] = replacement; - } - } - }); - return printer(newValue, config, indentation, depth, refs, printer); - }, - }; -} - -expect.addSnapshotSerializer( - replaceFieldValuesSerializer({ - header: '
', - // We do want to differentiate between zero and non-zero in these numbers. - durationNs: (v: number) => (v ? 12345 : 0), - sentTimeOffset: (v: number) => (v ? 23456 : 0), - // endTime and startTime are annoyingly used both for top-level Timestamps - // and for node-level nanosecond offsets. The Timestamps will get normalized - // by the nanos/seconds below. - startTime: (v: any) => (typeof v === 'string' ? '34567' : v), - endTime: (v: any) => (typeof v === 'string' ? '45678' : v), - nanos: 123000000, - seconds: '1562203363', - }), -); - -async function startFederatedServer(modules: GraphQLSchemaModule[]) { - const schema = buildFederatedSchema(modules); - const server = new ApolloServer({ schema }); - const { url } = await server.listen({ port: 0 }); - return { url, server }; -} - -describe('reporting', () => { - let backendServers: ApolloServer[]; - let gatewayServer: ApolloServer; - let gatewayUrl: string; - let reportPromise: Promise; - let nockScope: nock.Scope; - - beforeEach(async () => { - let reportResolver: (report: any) => void; - reportPromise = new Promise((resolve) => { - reportResolver = resolve; - }); - - nockScope = nock('https://engine-report.apollodata.com') - .post('/api/ingress/traces') - .reply(200, (_: any, requestBody: string) => { - reportResolver(requestBody); - return 'ok'; - }); - - backendServers = []; - const serviceList = []; - for (const fixture of fixtures) { - const { server, url } = await startFederatedServer([fixture]); - backendServers.push(server); - serviceList.push({ name: fixture.name, url }); - } - - const gateway = new ApolloGateway({ serviceList }); - const { schema, executor } = await gateway.load(); - gatewayServer = new ApolloServer({ - schema, - executor, - engine: { - apiKey: 'service:foo:bar', - sendReportsImmediately: true, - }, - }); - ({ url: gatewayUrl } = await gatewayServer.listen({ port: 0 })); - }); - - afterEach(async () => { - for (const server of backendServers) { - await server.stop(); - } - if (gatewayServer) { - await gatewayServer.stop(); - } - nockScope.done(); - }); - - it(`queries three services`, async () => { - const query = gql` - query { - me { - name { - first - last - } - } - topProducts { - name - } - } - `; - - const result = await toPromise( - execute(createHttpLink({ uri: gatewayUrl, fetch: fetch as any }), { - query, - }), - ); - expect(result).toMatchInlineSnapshot(` - Object { - "data": Object { - "me": Object { - "name": Object { - "first": "Ada", - "last": "Lovelace", - }, - }, - "topProducts": Array [ - Object { - "name": "Table", - }, - Object { - "name": "Couch", - }, - Object { - "name": "Chair", - }, - Object { - "name": "Structure and Interpretation of Computer Programs (1996)", - }, - Object { - "name": "Object Oriented Software Construction (1997)", - }, - ], - }, - } - `); - const reportBody = await reportPromise; - // nock returns binary bodies as hex strings - const gzipReportBuffer = Buffer.from(reportBody, 'hex'); - const reportBuffer = gunzipSync(gzipReportBuffer); - const report = Report.decode(reportBuffer); - - // Some handwritten tests to capture salient properties. - const statsReportKey = '# -\n{me{name{first last}}topProducts{name}}'; - expect(Object.keys(report.tracesPerQuery)).toStrictEqual([statsReportKey]); - expect(report.tracesPerQuery[statsReportKey]!.trace!.length).toBe(1); - const trace = report.tracesPerQuery[statsReportKey]!.trace![0]!; - // In the gateway, the root trace is just an empty node (unless there are errors). - expect(trace.root!.child).toStrictEqual([]); - // The query plan has (among other things) a fetch against 'accounts' and a - // fetch against 'product'. - expect(trace.queryPlan).toBeTruthy(); - const queryPlan = trace.queryPlan!; - expect(queryPlan.parallel).toBeTruthy(); - expect(queryPlan.parallel!.nodes![0]!.fetch!.serviceName).toBe('accounts'); - expect( - queryPlan.parallel!.nodes![0]!.fetch!.trace!.root!.child![0]! - .responseName, - ).toBe('me'); - expect(queryPlan.parallel!.nodes![1]!.sequence).toBeTruthy(); - expect( - queryPlan.parallel!.nodes![1]!.sequence!.nodes![0]!.fetch!.serviceName, - ).toBe('product'); - expect( - queryPlan.parallel!.nodes![1]!.sequence!.nodes![0]!.fetch!.trace!.root! - .child![0].responseName, - ).toBe('topProducts'); - - expect(report).toMatchInlineSnapshot(` - Object { - "endTime": null, - "header": "
", - "tracesPerQuery": Object { - "# - - {me{name{first last}}topProducts{name}}": Object { - "trace": Array [ - Object { - "clientName": "", - "clientReferenceId": "", - "clientVersion": "", - "details": Object {}, - "durationNs": 12345, - "endTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "forbiddenOperation": false, - "fullQueryCacheHit": false, - "http": Object { - "method": "POST", - }, - "queryPlan": Object { - "parallel": Object { - "nodes": Array [ - Object { - "fetch": Object { - "receivedTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTimeOffset": 23456, - "serviceName": "accounts", - "trace": Object { - "durationNs": 12345, - "endTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "root": Object { - "child": Array [ - Object { - "child": Array [ - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Name", - "responseName": "first", - "startTime": "34567", - "type": "String", - }, - Object { - "endTime": "45678", - "parentType": "Name", - "responseName": "last", - "startTime": "34567", - "type": "String", - }, - ], - "endTime": "45678", - "parentType": "User", - "responseName": "name", - "startTime": "34567", - "type": "Name", - }, - ], - "endTime": "45678", - "parentType": "Query", - "responseName": "me", - "startTime": "34567", - "type": "User", - }, - ], - }, - "startTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - }, - "traceParsingFailed": false, - }, - }, - Object { - "sequence": Object { - "nodes": Array [ - Object { - "fetch": Object { - "receivedTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTimeOffset": 23456, - "serviceName": "product", - "trace": Object { - "durationNs": 12345, - "endTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "root": Object { - "child": Array [ - Object { - "child": Array [ - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Furniture", - "responseName": "name", - "startTime": "34567", - "type": "String", - }, - ], - "index": 0, - }, - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Furniture", - "responseName": "name", - "startTime": "34567", - "type": "String", - }, - ], - "index": 1, - }, - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Furniture", - "responseName": "name", - "startTime": "34567", - "type": "String", - }, - ], - "index": 2, - }, - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "isbn", - "startTime": "34567", - "type": "String!", - }, - ], - "index": 3, - }, - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "isbn", - "startTime": "34567", - "type": "String!", - }, - ], - "index": 4, - }, - ], - "endTime": "45678", - "parentType": "Query", - "responseName": "topProducts", - "startTime": "34567", - "type": "[Product]", - }, - ], - }, - "startTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - }, - "traceParsingFailed": false, - }, - }, - Object { - "flatten": Object { - "node": Object { - "fetch": Object { - "receivedTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTimeOffset": 23456, - "serviceName": "books", - "trace": Object { - "durationNs": 12345, - "endTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "root": Object { - "child": Array [ - Object { - "child": Array [ - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "isbn", - "startTime": "34567", - "type": "String!", - }, - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "title", - "startTime": "34567", - "type": "String", - }, - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "year", - "startTime": "34567", - "type": "Int", - }, - ], - "index": 0, - }, - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "isbn", - "startTime": "34567", - "type": "String!", - }, - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "title", - "startTime": "34567", - "type": "String", - }, - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "year", - "startTime": "34567", - "type": "Int", - }, - ], - "index": 1, - }, - ], - "endTime": "45678", - "parentType": "Query", - "responseName": "_entities", - "startTime": "34567", - "type": "[_Entity]!", - }, - ], - }, - "startTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - }, - "traceParsingFailed": false, - }, - }, - "responsePath": Array [ - Object { - "fieldName": "topProducts", - }, - Object { - "fieldName": "@", - }, - ], - }, - }, - Object { - "flatten": Object { - "node": Object { - "fetch": Object { - "receivedTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "sentTimeOffset": 23456, - "serviceName": "product", - "trace": Object { - "durationNs": 12345, - "endTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - "root": Object { - "child": Array [ - Object { - "child": Array [ - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "name", - "startTime": "34567", - "type": "String", - }, - ], - "index": 0, - }, - Object { - "child": Array [ - Object { - "endTime": "45678", - "parentType": "Book", - "responseName": "name", - "startTime": "34567", - "type": "String", - }, - ], - "index": 1, - }, - ], - "endTime": "45678", - "parentType": "Query", - "responseName": "_entities", - "startTime": "34567", - "type": "[_Entity]!", - }, - ], - }, - "startTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - }, - "traceParsingFailed": false, - }, - }, - "responsePath": Array [ - Object { - "fieldName": "topProducts", - }, - Object { - "fieldName": "@", - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - "registeredOperation": false, - "root": Object {}, - "startTime": Object { - "nanos": 123000000, - "seconds": "1562203363", - }, - }, - ], - }, - }, - } - `); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/abstract-types.test.ts b/packages/apollo-gateway/src/__tests__/integration/abstract-types.test.ts deleted file mode 100644 index 2bad0c2710d..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/abstract-types.test.ts +++ /dev/null @@ -1,830 +0,0 @@ -import gql from 'graphql-tag'; -import { execute } from '../execution-utils'; - -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -it('handles an abstract type from the base service', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - upc - name - price - } - } - `; - - const upc = '1'; - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ - product: { - upc, - name: 'Table', - price: '899', - }, - }); - - expect(queryPlan).toCallService('product'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - product(upc: $upc) { - __typename - ... on Book { - upc - __typename - isbn - price - } - ... on Furniture { - upc - name - price - } - } - } - }, - Flatten(path: "product") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - __typename - isbn - title - year - } - } - }, - }, - Flatten(path: "product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - } - `); -}); - -it('can request fields on extended interfaces', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - inStock - } - } - `; - - const upc = '1'; - - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ product: { inStock: true } }); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toCallService('inventory'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - product(upc: $upc) { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - sku - } - } - } - }, - Flatten(path: "product") { - Fetch(service: "inventory") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - sku - } - } => - { - ... on Book { - inStock - } - ... on Furniture { - inStock - } - } - }, - }, - }, - } - `); -}); - -it('can request fields on extended types that implement an interface', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - inStock - ... on Furniture { - isHeavy - } - } - } - `; - - const upc = '1'; - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ product: { inStock: true, isHeavy: false } }); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toCallService('inventory'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - product(upc: $upc) { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - sku - } - } - } - }, - Flatten(path: "product") { - Fetch(service: "inventory") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - sku - } - } => - { - ... on Book { - inStock - } - ... on Furniture { - inStock - isHeavy - } - } - }, - }, - }, - } - `); -}); - -it('prunes unfilled type conditions', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - inStock - ... on Furniture { - isHeavy - } - ... on Book { - isCheckedOut - } - } - } - `; - - const upc = '1'; - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ product: { inStock: true, isHeavy: false } }); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toCallService('inventory'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - product(upc: $upc) { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - sku - } - } - } - }, - Flatten(path: "product") { - Fetch(service: "inventory") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - sku - } - } => - { - ... on Book { - inStock - isCheckedOut - } - ... on Furniture { - inStock - isHeavy - } - } - }, - }, - }, - } - `); -}); - -it('fetches interfaces returned from other services', async () => { - const query = `#graphql - query GetUserAndProducts { - me { - reviews { - product { - price - ... on Book { - title - } - } - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - reviews: [ - { product: { price: '899' } }, - { product: { price: '1299' } }, - { product: { price: '49', title: 'Design Patterns' } }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "accounts") { - { - me { - __typename - id - } - } - }, - Flatten(path: "me") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - product { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } - } - } - } - }, - }, - Parallel { - Flatten(path: "me.reviews.@.product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } => - { - ... on Book { - price - } - ... on Furniture { - price - } - } - }, - }, - Flatten(path: "me.reviews.@.product") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - title - } - } - }, - }, - }, - }, - } - `); -}); - -it('fetches composite fields from a foreign type casted to an interface [@provides field]', async () => { - const query = `#graphql - query GetUserAndProducts { - me { - reviews { - product { - price - ... on Book { - name - } - } - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - reviews: [ - { product: { price: '899' } }, - { product: { price: '1299' } }, - { product: { price: '49', name: 'Design Patterns (1995)' } }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "accounts") { - { - me { - __typename - id - } - } - }, - Flatten(path: "me") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - product { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } - } - } - } - }, - }, - Parallel { - Flatten(path: "me.reviews.@.product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } => - { - ... on Book { - price - } - ... on Furniture { - price - } - } - }, - }, - Sequence { - Flatten(path: "me.reviews.@.product") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - __typename - isbn - title - year - } - } - }, - }, - Flatten(path: "me.reviews.@.product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - title - year - } - } => - { - ... on Book { - name - } - } - }, - }, - }, - }, - }, - } - `); -}); - -it('allows for extending an interface from another service with fields', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - reviews { - body - } - } - } - `; - - const upc = '1'; - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ - product: { - reviews: [{ body: 'Love it!' }, { body: 'Prefer something else.' }], - }, - }); - - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - product(upc: $upc) { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } - } - }, - Flatten(path: "product") { - Fetch(service: "reviews") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } => - { - ... on Book { - reviews { - body - } - } - ... on Furniture { - reviews { - body - } - } - } - }, - }, - }, - } - `); -}); - -describe('unions', () => { - it('handles unions from the same service', async () => { - const query = `#graphql - query GetUserAndProducts { - me { - reviews { - product { - price - ... on Furniture { - brand { - ... on Ikea { - asile - } - ... on Amazon { - referrer - } - } - } - } - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - reviews: [ - { product: { price: '899', brand: { asile: 10 } } }, - { - product: { - price: '1299', - brand: { referrer: 'https://canopy.co' }, - }, - }, - { product: { price: '49' } }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "accounts") { - { - me { - __typename - id - } - } - }, - Flatten(path: "me") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - product { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } - } - } - } - }, - }, - Flatten(path: "me.reviews.@.product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } => - { - ... on Book { - price - } - ... on Furniture { - price - brand { - __typename - ... on Ikea { - asile - } - ... on Amazon { - referrer - } - } - } - } - }, - }, - }, - } - `); - }); - - it("doesn't expand interfaces with inline type conditions if all possibilities are fufilled by one service", async () => { - const query = `#graphql - query GetProducts { - topProducts { - name - } - } - `; - - const { queryPlan, errors } = await execute({ query }, [ - { - name: 'products', - typeDefs: gql` - extend type Query { - topProducts: [Product] - } - - interface Product { - name: String - } - - type Shoe implements Product { - name: String - } - - type Car implements Product { - name: String - } - `, - }, - ]); - - expect(errors).toBeUndefined(); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "products") { - { - topProducts { - __typename - name - } - } - }, - } - `); - }); - - // FIXME: turn back on when extending unions is supported in composition - it.todo('fetches unions across services'); - // async () => { - // const query = gql` - // query GetUserAndProducts { - // me { - // account { - // ... on LibraryAccount { - // library { - // name - // } - // } - // ... on SMSAccount { - // number - // } - // } - // } - // } - // `; - - // const { data, queryPlan } = await execute( - // { - // query, - // }, - // ); - - // expect(data).toEqual({ - // me: { - // account: { - // library: { - // name: 'NYC Public Library', - // }, - // }, - // }, - // }); - - // expect(queryPlan).toCallService('accounts'); - // expect(queryPlan).toCallService('books'); - // }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/aliases.test.ts b/packages/apollo-gateway/src/__tests__/integration/aliases.test.ts deleted file mode 100644 index e86d7d2ecc2..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/aliases.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { execute } from '../execution-utils'; -// FIXME: remove this when GraphQLExtensions is removed -import { createTestClient } from 'apollo-server-testing'; -import { ApolloServerBase as ApolloServer } from 'apollo-server-core'; -import { buildFederatedSchema } from '@apollo/federation'; -import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource'; -import { ApolloGateway } from '../../'; -import { fixtures } from 'apollo-federation-integration-testsuite'; - -it('supports simple aliases', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - name - title: name - } - } - `; - - const upc = '1'; - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ - product: { - name: 'Table', - title: 'Table', - }, - }); - - expect(queryPlan).toCallService('product'); -}); - -it('supports aliases of root fields on subservices', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - name - title: name - reviews { - body - } - productReviews: reviews { - body - } - } - } - `; - - const upc = '1'; - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ - product: { - name: 'Table', - title: 'Table', - reviews: [ - { - body: 'Love it!', - }, - { - body: 'Prefer something else.', - }, - ], - productReviews: [ - { - body: 'Love it!', - }, - { - body: 'Prefer something else.', - }, - ], - }, - }); - - expect(queryPlan).toCallService('product'); -}); - -it('supports aliases of nested fields on subservices', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - name - title: name - reviews { - content: body - body - } - productReviews: reviews { - body - reviewer: author { - name: username - } - } - } - } - `; - - const upc = '1'; - const { data, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(data).toEqual({ - product: { - name: 'Table', - title: 'Table', - reviews: [ - { - content: 'Love it!', - body: 'Love it!', - }, - { - content: 'Prefer something else.', - body: 'Prefer something else.', - }, - ], - productReviews: [ - { - body: 'Love it!', - reviewer: { - name: '@ada', - }, - }, - { - body: 'Prefer something else.', - reviewer: { - name: '@complete', - }, - }, - ], - }, - }); - - expect(queryPlan).toCallService('product'); -}); - -// TODO after we remove GraphQLExtensions from ApolloServer, this can go away -it('supports aliases when using ApolloServer', async () => { - const gateway = new ApolloGateway({ - localServiceList: fixtures, - buildService: service => { - return new LocalGraphQLDataSource(buildFederatedSchema([service])); - }, - }); - - const { schema, executor } = await gateway.load(); - - const server = new ApolloServer({ schema, executor }); - - const upc = '1'; - const { query } = createTestClient(server); - - const result = await query({ - query: `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - title: name - } - } - `, - variables: { upc }, - }); - - expect(result.data).toEqual({ - product: { - title: 'Table', - }, - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/boolean.test.ts b/packages/apollo-gateway/src/__tests__/integration/boolean.test.ts deleted file mode 100644 index 2e50eb8375d..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/boolean.test.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -// TODO: right now the query planner doesn't prune known skip and include points -// eventually we want to do this to prevent downstream fetches that aren't needed -describe('@skip', () => { - it('supports @skip when a boolean condition is met', async () => { - const query = `#graphql - query GetReviewers { - topReviews { - body - author @skip(if: true) { - name - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'Could be better.' }, - { body: 'Prefer something else.' }, - { body: 'Wish I had read this before.' }, - ], - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); - - it('supports @skip when a boolean condition is met (variable driven)', async () => { - const query = `#graphql - query GetReviewers($skip: Boolean!) { - topReviews { - body - author @skip(if: $skip) { - username - } - } - } - `; - - const skip = true; - const { data, queryPlan } = await execute({ - query, - variables: { skip }, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'Could be better.' }, - { body: 'Prefer something else.' }, - { body: 'Wish I had read this before.' }, - ], - }); - - expect(queryPlan).not.toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); - - // Data looks good here, suspect the matcher is incorrect - it('supports @skip when a boolean condition is not met', async () => { - const query = `#graphql - query GetReviewers { - topReviews { - body - author @skip(if: false) { - name { - first - last - } - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, - ], - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); - - // Data looks good here, suspect the matcher is incorrect - it('supports @skip when a boolean condition is not met (variable driven)', async () => { - const query = `#graphql - query GetReviewers($skip: Boolean!) { - topReviews { - body - author @skip(if: $skip) { - name { - first - last - } - } - } - } - `; - - const skip = false; - const { data, queryPlan } = await execute({ - query, - variables: { skip }, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, - ], - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); -}); - -describe('@include', () => { - it('supports @include when a boolean condition is not met', async () => { - const query = `#graphql - query GetReviewers { - topReviews { - body - author @include(if: false) { - username - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'Could be better.' }, - { body: 'Prefer something else.' }, - { body: 'Wish I had read this before.' }, - ], - }); - - expect(queryPlan).not.toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); - - it('supports @include when a boolean condition is not met (variable driven)', async () => { - const query = `#graphql - query GetReviewers($include: Boolean!) { - topReviews { - body - author @include(if: $include) { - username - } - } - } - `; - - const include = false; - const { data, queryPlan } = await execute({ - query, - variables: { include }, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'Could be better.' }, - { body: 'Prefer something else.' }, - { body: 'Wish I had read this before.' }, - ], - }); - - expect(queryPlan).not.toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); - - // Data looks good here, suspect the matcher is incorrect - // Added the query plan snapshot for a view. - it('supports @include when a boolean condition is met', async () => { - const query = `#graphql - query GetReviewers { - topReviews { - body - author @include(if: true) { - name { - first - last - } - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, - ], - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); - - // Data looks good here, suspect the matcher is incorrect - // Added the query plan snapshot for a view. - it('supports @include when a boolean condition is met (variable driven)', async () => { - const query = `#graphql - query GetReviewers($include: Boolean!) { - topReviews { - body - author @include(if: $include) { - name { - first - last - } - } - } - } - `; - - const include = true; - const { data, queryPlan } = await execute({ - query, - variables: { include }, - }); - - expect(data).toEqual({ - topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, - ], - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/complex-key.test.ts b/packages/apollo-gateway/src/__tests__/integration/complex-key.test.ts deleted file mode 100644 index a413c3a7ad1..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/complex-key.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import gql from 'graphql-tag'; -import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -const users = [ - { id: '1', name: 'Trevor Scheer', organizationId: '1', __typename: 'User' }, - { id: '1', name: 'Trevor Scheer', organizationId: '2', __typename: 'User' }, - { id: '2', name: 'James Baxley', organizationId: '1', __typename: 'User' }, - { id: '2', name: 'James Baxley', organizationId: '3', __typename: 'User' }, -]; - -const organizations = [ - { id: '1', name: 'Apollo', __typename: 'Organization' }, - { id: '2', name: 'Wayfair', __typename: 'Organization' }, - { id: '3', name: 'Major League Soccer', __typename: 'Organization' }, -]; - -const reviews = [ - { id: '1', authorId: '1', organizationId: '1', __typename: 'Review' }, - { id: '2', authorId: '1', organizationId: '2', __typename: 'Review' }, - { id: '3', authorId: '2', organizationId: '1', __typename: 'Review' }, - { id: '4', authorId: '2', organizationId: '3', __typename: 'Review' }, -]; - -const reviewService: ServiceDefinitionModule = { - name: 'review', - typeDefs: gql` - type Query { - reviews: [Review!]! - } - - type Review { - id: ID! - author: User! - body: String! - } - - # TODO: consider ergonomics of external types. - extend type User @key(fields: "id organization { id }") { - id: ID! @external - organization: Organization! @external - } - - extend type Organization { - id: ID! @external - } - `, - resolvers: { - Query: { - reviews() { - return reviews; - }, - }, - Review: { - author(review) { - return { - id: review.authorId, - organization: { - id: review.organizationId, - }, - }; - }, - }, - }, -}; - -const userService: ServiceDefinitionModule = { - name: 'user', - typeDefs: gql` - type User @key(fields: "id organization { id }") { - id: ID! - name: String! - organization: Organization! - } - - type Organization @key(fields: "id") { - id: ID! - name: String! - } - `, - resolvers: { - User: { - __resolveReference(reference) { - return users.find( - user => - user.id === reference.id && - user.organizationId === reference.organization.id, - ); - }, - organization(user) { - return { id: user.organizationId }; - }, - }, - Organization: { - __resolveObject(object) { - return organizations.find(org => org.id === object.id); - }, - }, - }, -}; - -it('works fetches data correctly with complex / nested @key fields', async () => { - const query = `#graphql - query Reviews { - reviews { - author { - name - organization { - name - } - } - } - } - `; - - const { data, queryPlan } = await execute( - { - query, - }, - [userService, reviewService], - ); - - expect(data).toEqual({ - reviews: [ - { - author: { - name: 'Trevor Scheer', - organization: { - name: 'Apollo', - }, - }, - }, - { - author: { - name: 'Trevor Scheer', - organization: { - name: 'Wayfair', - }, - }, - }, - { - author: { - name: 'James Baxley', - organization: { - name: 'Apollo', - }, - }, - }, - { - author: { - name: 'James Baxley', - organization: { - name: 'Major League Soccer', - }, - }, - }, - ], - }); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "review") { - { - reviews { - author { - __typename - id - organization { - id - __typename - } - } - } - } - }, - Parallel { - Flatten(path: "reviews.@.author") { - Fetch(service: "user") { - { - ... on User { - __typename - id - organization { - id - } - } - } => - { - ... on User { - name - } - } - }, - }, - Flatten(path: "reviews.@.author.organization") { - Fetch(service: "user") { - { - ... on Organization { - __typename - id - } - } => - { - ... on Organization { - name - } - } - }, - }, - }, - }, - } - `); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/custom-directives.test.ts b/packages/apollo-gateway/src/__tests__/integration/custom-directives.test.ts deleted file mode 100644 index 5b490fbb726..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/custom-directives.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import gql from 'graphql-tag'; -import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; -import { fixtures } from 'apollo-federation-integration-testsuite'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -describe('custom executable directives', () => { - it('successfully passes directives along in requests to an underlying service', async () => { - const query = `#graphql - query GetReviewers { - topReviews { - body @stream - } - } - `; - - const { errors, queryPlan } = await execute({ - query, - }); - - expect(errors).toBeUndefined(); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "reviews") { - { - topReviews { - body @stream - } - } - }, - } - `); - }); - - it('successfully passes directives and their variables along in requests to underlying services', async () => { - const query = `#graphql - query GetReviewers { - topReviews { - body @stream - author @transform(from: "JSON") { - name @stream { - first - last - } - } - } - } - `; - - const { errors, queryPlan } = await execute({ - query, - }); - - expect(errors).toBeUndefined(); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - topReviews { - body @stream - author @transform(from: "JSON") { - __typename - id - } - } - } - }, - Flatten(path: "topReviews.@.author") { - Fetch(service: "accounts") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - name @stream { - first - last - } - } - } - }, - }, - }, - } - `); - }); - - it("returns validation errors when directives aren't present across all services", async () => { - const invalidService = { - name: 'invalidService', - typeDefs: gql` - directive @invalid on QUERY - `, - }; - - const query = `#graphql - query GetReviewers { - topReviews { - body @stream - } - } - `; - - expect( - execute( - { - query, - }, - [...fixtures, invalidService], - ), - ).rejects.toThrowErrorMatchingInlineSnapshot(` -"[@stream] -> Custom directives must be implemented in every service. The following services do not implement the @stream directive: invalidService. - -[@transform] -> Custom directives must be implemented in every service. The following services do not implement the @transform directive: invalidService. - -[@invalid] -> Custom directives must be implemented in every service. The following services do not implement the @invalid directive: accounts, books, documents, inventory, product, reviews." -`); - }); - - it("returns validation errors when directives aren't identical across all services", async () => { - const invalidService = { - name: 'invalid', - typeDefs: gql` - directive @stream on QUERY - `, - }; - - const query = `#graphql - query GetReviewers { - topReviews { - body @stream - } - } - `; - - expect( - execute( - { - query, - }, - [...fixtures, invalidService], - ), - ).rejects.toThrowErrorMatchingInlineSnapshot(` -"[@transform] -> Custom directives must be implemented in every service. The following services do not implement the @transform directive: invalid. - -[@stream] -> custom directives must be defined identically across all services. See below for a list of current implementations: - accounts: directive @stream on FIELD - books: directive @stream on FIELD - documents: directive @stream on FIELD - inventory: directive @stream on FIELD - product: directive @stream on FIELD - reviews: directive @stream on FIELD - invalid: directive @stream on QUERY" -`); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/execution-style.test.ts b/packages/apollo-gateway/src/__tests__/integration/execution-style.test.ts deleted file mode 100644 index 30a1088ac34..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/execution-style.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { execute } from '../execution-utils'; - -describe('query', () => { - it('supports parallel root fields', async () => { - const query = `#graphql - query GetUserAndReviews { - me { - username - } - topReviews { - body - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { username: '@ada' }, - topReviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'Could be better.' }, - { body: 'Prefer something else.' }, - { body: 'Wish I had read this before.' }, - ], - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - // FIXME: determine matcher for execution order - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/fragments.test.ts b/packages/apollo-gateway/src/__tests__/integration/fragments.test.ts deleted file mode 100644 index 939cac59c57..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/fragments.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { disableFragmentWarnings } from 'graphql-tag'; -import { execute } from '../execution-utils'; - -beforeAll(() => { - disableFragmentWarnings(); -}); -it('supports inline fragments (one level)', async () => { - const query = `#graphql - query GetUser { - me { - ... on User { - username - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - username: '@ada', - }, - }); - - expect(queryPlan).toCallService('accounts'); -}); - -it('supports inline fragments (multi level)', async () => { - const query = `#graphql - query GetUser { - me { - ... on User { - username - reviews { - ... on Review { - body - product { - ... on Product { - ... on Book { - title - } - ... on Furniture { - name - } - } - } - } - } - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - username: '@ada', - reviews: [ - { body: 'Love it!', product: { name: 'Table' } }, - { body: 'Too expensive.', product: { name: 'Couch' } }, - { body: 'A classic.', product: { title: 'Design Patterns' } }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toCallService('books'); -}); - -it('supports named fragments (one level)', async () => { - const query = `#graphql - query GetUser { - me { - ...userDetails - } - } - - fragment userDetails on User { - username - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - username: '@ada', - }, - }); - - expect(queryPlan).toCallService('accounts'); -}); - -it('supports multiple named fragments (one level, mixed ordering)', async () => { - const query = `#graphql - fragment userInfo on User { - name { - first - last - } - } - query GetUser { - me { - ...userDetails - ...userInfo - } - } - - fragment userDetails on User { - username - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - username: '@ada', - name: { - first: 'Ada', - last: 'Lovelace', - } - }, - }); - - expect(queryPlan).toCallService('accounts'); -}); - -it('supports multiple named fragments (multi level, mixed ordering)', async () => { - const query = `#graphql - fragment reviewDetails on Review { - body - } - query GetUser { - me { - ...userDetails - } - } - - fragment userDetails on User { - username - reviews { - ...reviewDetails - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - reviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'A classic.' }, - ], - username: '@ada', - }, - }); - - expect(queryPlan).toCallService('accounts'); -}); - -it('supports variables within fragments', async () => { - const query = `#graphql - query GetUser($format: Boolean) { - me { - ...userDetails - } - } - - fragment userDetails on User { - username - reviews { - body(format: $format) - } - } - `; - - const format = true; - const { data, queryPlan } = await execute({ - query, - variables: { format }, - }); - - expect(data).toEqual({ - me: { - username: '@ada', - reviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { body: 'A classic.' }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); -}); - -it('supports root fragments', async () => { - const query = `#graphql - query GetUser { - ... on Query { - me { - username - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - username: '@ada', - }, - }); - - expect(queryPlan).toCallService('accounts'); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/list-key.test.ts b/packages/apollo-gateway/src/__tests__/integration/list-key.test.ts deleted file mode 100644 index 1573ecd99fe..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/list-key.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import gql from 'graphql-tag'; -import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -const users = [ - { id: ['1', '1'], name: 'Trevor Scheer', __typename: 'User' }, - { id: ['2', '2'], name: 'James Baxley', __typename: 'User' }, -]; - -const reviews = [ - { id: '1', authorId: ['1', '1'], body: 'Good', __typename: 'Review' }, - { id: '2', authorId: ['2', '2'], body: 'Bad', __typename: 'Review' }, -]; - -const reviewService: ServiceDefinitionModule = { - name: 'review', - typeDefs: gql` - type Query { - reviews: [Review!]! - } - - type Review { - id: ID! - author: User! - body: String! - } - - extend type User @key(fields: "id") { - id: [ID!]! @external - } - `, - resolvers: { - Query: { - reviews() { - return reviews; - }, - }, - Review: { - author(review) { - return { - id: review.authorId, - }; - }, - }, - }, -}; - -const listsAreEqual = (as: T[], bs: T[]) => - as.length === bs.length && as.every((a, i) => bs[i] === as[i]); - -const userService: ServiceDefinitionModule = { - name: 'user', - typeDefs: gql` - type User @key(fields: "id") { - id: [ID!]! - name: String! - } - `, - resolvers: { - User: { - __resolveReference(reference) { - return users.find(user => listsAreEqual(user.id, reference.id)); - }, - }, - }, -}; - -it('fetches data correctly list type @key fields', async () => { - const query = `#graphql - query Reviews { - reviews { - body - author { - name - } - } - } - `; - - const { data, queryPlan } = await execute( - { - query, - }, - [userService, reviewService], - ); - - expect(data).toEqual({ - reviews: [ - { body: 'Good', author: { name: 'Trevor Scheer' } }, - { body: 'Bad', author: { name: 'James Baxley' } }, - ], - }); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "review") { - { - reviews { - body - author { - __typename - id - } - } - } - }, - Flatten(path: "reviews.@.author") { - Fetch(service: "user") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - name - } - } - }, - }, - }, - } - `); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/logger.test.ts b/packages/apollo-gateway/src/__tests__/integration/logger.test.ts deleted file mode 100644 index 690c046d421..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/logger.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { ApolloGateway } from '../..'; -import { Logger } from "apollo-server-types"; -import { PassThrough } from "stream"; - -import * as winston from "winston"; -import WinstonTransport from 'winston-transport'; -import * as bunyan from "bunyan"; -import * as loglevel from "loglevel"; -// We are testing an older version of `log4js` which uses older ECMAScript -// in order to still support testing on Node.js 6. -// This should be updated when bump the semver major for AS3. -import * as log4js from "log4js"; - -const LOWEST_LOG_LEVEL = "debug"; - -const KNOWN_DEBUG_MESSAGE = "Checking service definitions..."; - -async function triggerKnownDebugMessage(logger: Logger) { - // Trigger a known error. - // This is a bit brittle since it merely leverages a known debug log - // message outside of the constructor, but it seemed worth testing - // the compatibility with `ApolloGateway` itself rather than generically. - // The error does not matter, so it is caught and ignored. - await new ApolloGateway({ logger }).load().catch(_e => {}); -} - -describe("logger", () => { - it("works with 'winston'", async () => { - const sink = jest.fn(); - const transport = new class extends WinstonTransport { - constructor() { - super({ - format: winston.format.json(), - }); - } - - log(info: any) { - sink(info); - } - }; - - const logger = winston.createLogger({ level: 'debug' }).add(transport); - - await triggerKnownDebugMessage(logger); - - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: LOWEST_LOG_LEVEL, - message: KNOWN_DEBUG_MESSAGE, - })); - }); - - it("works with 'bunyan'", async () => { - const sink = jest.fn(); - - // Bunyan uses streams for its logging implementations. - const writable = new PassThrough(); - writable.on("data", data => sink(JSON.parse(data.toString()))); - - const logger = bunyan.createLogger({ - name: "test-logger-bunyan", - streams: [{ - level: LOWEST_LOG_LEVEL, - stream: writable, - }] - }); - - await triggerKnownDebugMessage(logger); - - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: bunyan.DEBUG, - msg: KNOWN_DEBUG_MESSAGE, - })); - }); - - it("works with 'loglevel'", async () => { - const sink = jest.fn(); - - const logger = loglevel.getLogger("test-logger-loglevel") - logger.methodFactory = (_methodName, level): loglevel.LoggingMethod => - (message) => sink({ level, message }); - - // The `setLevel` method must be called after overwriting `methodFactory`. - // This is an intentional API design pattern of the loglevel package: - // https://www.npmjs.com/package/loglevel#writing-plugins - logger.setLevel(loglevel.levels.DEBUG); - - await triggerKnownDebugMessage(logger); - - expect(sink).toHaveBeenCalledWith({ - level: loglevel.levels.DEBUG, - message: KNOWN_DEBUG_MESSAGE, - }); - }); - - it("works with 'log4js'", async () => { - const sink = jest.fn(); - - log4js.configure({ - appenders: { - custom: { - type: { - configure: () => - (loggingEvent: log4js.LoggingEvent) => sink(loggingEvent) - } - } - }, - categories: { - default: { - appenders: ['custom'], - level: LOWEST_LOG_LEVEL, - } - } - }); - - const logger = log4js.getLogger(); - logger.level = LOWEST_LOG_LEVEL; - - await triggerKnownDebugMessage(logger); - - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: log4js.levels.DEBUG, - data: [KNOWN_DEBUG_MESSAGE], - })); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/merge-arrays.test.ts b/packages/apollo-gateway/src/__tests__/integration/merge-arrays.test.ts deleted file mode 100644 index 2748b0d5753..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/merge-arrays.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { execute } from '../execution-utils'; - -describe('query', () => { - it('supports arrays', async () => { - const query = `#graphql - query MergeArrays { - me { - # goodAddress - goodDescription - metadata { - address - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - goodDescription: true, - metadata: [ - { - address: '1', - }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/multiple-key.test.ts b/packages/apollo-gateway/src/__tests__/integration/multiple-key.test.ts deleted file mode 100644 index 60450252d46..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/multiple-key.test.ts +++ /dev/null @@ -1,328 +0,0 @@ -import gql from 'graphql-tag'; -import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -const users = [ - { ssn: '111-11-1111', name: 'Trevor', id: '10', __typename: 'User' }, - { ssn: '222-22-2222', name: 'Scheer', id: '20', __typename: 'User' }, - { ssn: '333-33-3333', name: 'James', id: '30', __typename: 'User' }, - { ssn: '444-44-4444', name: 'Baxley', id: '40', __typename: 'User' }, -]; - -const reviews = [ - { id: '1', authorId: '10', body: 'A', __typename: 'Review' }, - { id: '2', authorId: '20', body: 'B', __typename: 'Review' }, - { id: '3', authorId: '30', body: 'C', __typename: 'Review' }, - { id: '4', authorId: '40', body: 'D', __typename: 'Review' }, -]; - -const reviewService: ServiceDefinitionModule = { - name: 'reviews', - typeDefs: gql` - extend type Query { - reviews: [Review!]! - } - - type Review { - id: ID! - author: User! - body: String! - } - - extend type User @key(fields: "id") { - id: ID! @external - reviews: [Review!]! - } - `, - resolvers: { - Query: { - reviews() { - return reviews; - }, - }, - User: { - reviews(user) { - return reviews.filter(review => review.authorId === user.id); - }, - }, - Review: { - author(review) { - return { - id: review.authorId, - }; - }, - }, - }, -}; - -const actuaryService: ServiceDefinitionModule = { - name: 'actuary', - typeDefs: gql` - extend type User @key(fields: "ssn") { - ssn: ID! @external - risk: Float - } - `, - resolvers: { - User: { - risk(user) { - return user.ssn[0] / 10; - }, - }, - }, -}; - -const userService: ServiceDefinitionModule = { - name: 'users', - typeDefs: gql` - extend type Query { - users: [User!]! - } - - type Group { - id: ID - name: String - } - - type User - @key(fields: "ssn") - @key(fields: "id") - @key(fields: "group { id }") { - id: ID! - ssn: ID! - name: String! - group: Group - } - `, - resolvers: { - Query: { - users() { - return users; - }, - }, - User: { - group: () => ({ id: 1, name: 'Apollo GraphQL' }), - __resolveReference(reference) { - if (reference.ssn) - return users.find(user => user.ssn === reference.ssn); - else return users.find(user => user.id === reference.id); - }, - }, - }, -}; - -it('fetches data correctly with multiple @key fields', async () => { - const query = `#graphql - query { - reviews { - body - author { - name - risk - } - } - } - `; - - const { data, queryPlan, errors } = await execute( - { - query, - }, - [userService, reviewService, actuaryService], - ); - - expect(errors).toBeFalsy(); - expect(data).toEqual({ - reviews: [ - { - body: 'A', - author: { - risk: 0.1, - name: 'Trevor', - }, - }, - { - body: 'B', - author: { - risk: 0.2, - name: 'Scheer', - }, - }, - { - body: 'C', - author: { - risk: 0.3, - name: 'James', - }, - }, - { - body: 'D', - author: { - risk: 0.4, - name: 'Baxley', - }, - }, - ], - }); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - reviews { - body - author { - __typename - id - } - } - } - }, - Flatten(path: "reviews.@.author") { - Fetch(service: "users") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - name - __typename - ssn - } - } - }, - }, - Flatten(path: "reviews.@.author") { - Fetch(service: "actuary") { - { - ... on User { - __typename - ssn - } - } => - { - ... on User { - risk - } - } - }, - }, - }, - } - `); -}); - -it('fetches keys as needed to reduce round trip queries', async () => { - const query = `#graphql - query { - users { - risk - reviews { - body - } - } - } - `; - - const { data, queryPlan, errors } = await execute( - { - query, - }, - [userService, reviewService, actuaryService] - ); - - expect(errors).toBeFalsy(); - expect(data).toEqual({ - users: [ - { - risk: 0.1, - reviews: [ - { - body: 'A', - }, - ], - }, - { - risk: 0.2, - reviews: [ - { - body: 'B', - }, - ], - }, - { - risk: 0.3, - reviews: [ - { - body: 'C', - }, - ], - }, - { - risk: 0.4, - reviews: [ - { - body: 'D', - }, - ], - }, - ], - }); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "users") { - { - users { - __typename - ssn - id - } - } - }, - Parallel { - Flatten(path: "users.@") { - Fetch(service: "actuary") { - { - ... on User { - __typename - ssn - } - } => - { - ... on User { - risk - } - } - }, - }, - Flatten(path: "users.@") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - body - } - } - } - }, - }, - }, - }, - } - `); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts b/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts deleted file mode 100644 index fef0a5089b2..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; -import { accounts, reviews } from 'apollo-federation-integration-testsuite'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -function spyOnResolver(resolverMap: any, resolverName: T) { - return jest.spyOn(resolverMap, resolverName).mockName(resolverName); -} - -it('supports mutations', async () => { - const query = `#graphql - mutation Login($username: String!, $password: String!) { - login(username: $username, password: $password) { - reviews { - product { - upc - } - } - } - } - `; - - const variables = { username: '@complete', password: 'css_completes_me' }; - const { data, queryPlan } = await execute({ - query, - variables, - }); - - expect(data).toEqual({ - login: { - reviews: [ - { product: { upc: '3' } }, - { product: { upc: '1' } }, - { product: { upc: '0262510871' } }, - { product: { upc: '0136291554' } }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); -}); - -it('returning across service boundaries', async () => { - const query = `#graphql - mutation Review($upc: String!, $body: String!) { - reviewProduct(upc: $upc, body: $body) { - ... on Furniture { - name - } - } - } - `; - - const variables = { upc: '1', body: 'A great table' }; - const { data, queryPlan } = await execute({ - query, - variables, - }); - - expect(data).toEqual({ - reviewProduct: { - name: 'Table', - }, - }); - - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); -}); - -it('multiple root mutations', async () => { - const login = spyOnResolver(accounts.resolvers.Mutation, 'login'); - const reviewProduct = spyOnResolver( - reviews.resolvers.Mutation, - 'reviewProduct', - ); - - const query = `#graphql - mutation LoginAndReview( - $username: String! - $password: String! - $upc: String! - $body: String! - ) { - login(username: $username, password: $password) { - reviews { - product { - upc - } - } - } - reviewProduct(upc: $upc, body: $body) { - ... on Furniture { - name - } - } - } - `; - - const variables = { - username: '@complete', - password: 'css_completes_me', - upc: '1', - body: 'A great table.', - }; - const { data, queryPlan } = await execute({ - query, - variables, - }); - - expect(data).toEqual({ - login: { - reviews: [ - { product: { upc: '3' } }, - { product: { upc: '1' } }, - { product: { upc: '0262510871' } }, - { product: { upc: '0136291554' } }, - ], - }, - reviewProduct: { - name: 'Table', - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); - - expect(login).toHaveBeenCalledBefore(reviewProduct); -}); - -it('multiple root mutations with correct service order', async () => { - const reviewsMutations = reviews.resolvers.Mutation; - const reviewProduct = spyOnResolver(reviewsMutations, 'reviewProduct'); - const login = spyOnResolver(accounts.resolvers.Mutation, 'login'); - const updateReview = spyOnResolver(reviewsMutations, 'updateReview'); - const deleteReview = spyOnResolver(reviewsMutations, 'deleteReview'); - - const query = `#graphql - mutation LoginAndReview( - $upc: String! - $body: String! - $updatedReview: UpdateReviewInput! - $username: String! - $password: String! - $reviewId: ID! - ) { - reviewProduct(upc: $upc, body: $body) { - ... on Furniture { - upc - } - } - updateReview(review: $updatedReview) { - id - body - } - login(username: $username, password: $password) { - reviews { - product { - upc - } - } - } - deleteReview(id: $reviewId) - } - `; - - const variables = { - upc: '1', - body: 'A great table.', - updatedReview: { - id: '1', - body: 'An excellent table.', - }, - username: '@complete', - password: 'css_completes_me', - reviewId: '6', - }; - const { data, queryPlan } = await execute({ - query, - variables, - }); - - expect(data).toEqual({ - deleteReview: true, - login: { - reviews: [ - { product: { upc: '3' } }, - { product: { upc: '1' } }, - { product: { upc: '0262510871' } }, - { product: { upc: '0136291554' } }, - ], - }, - reviewProduct: { - upc: '1', - }, - updateReview: { - body: 'An excellent table.', - id: '1', - }, - }); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "reviews") { - { - reviewProduct(upc: $upc, body: $body) { - __typename - ... on Furniture { - upc - } - } - updateReview(review: $updatedReview) { - id - body - } - } - }, - Fetch(service: "accounts") { - { - login(username: $username, password: $password) { - __typename - id - } - } - }, - Flatten(path: "login") { - Fetch(service: "reviews") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - reviews { - product { - __typename - ... on Book { - __typename - isbn - } - ... on Furniture { - upc - } - } - } - } - } - }, - }, - Flatten(path: "login.reviews.@.product") { - Fetch(service: "product") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - upc - } - } - }, - }, - Fetch(service: "reviews") { - { - deleteReview(id: $reviewId) - } - }, - }, - } - `); - - expect(reviewProduct).toHaveBeenCalledBefore(updateReview); - expect(updateReview).toHaveBeenCalledBefore(login); - expect(login).toHaveBeenCalledBefore(deleteReview); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts deleted file mode 100644 index 5d3c1068144..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ /dev/null @@ -1,472 +0,0 @@ -import nock from 'nock'; -import { fetch } from 'apollo-server-env'; -import { Logger } from 'apollo-server-types'; -import { ApolloGateway, GCS_RETRY_COUNT, getDefaultGcsFetcher } from '../..'; -import { - mockSDLQuerySuccess, - mockServiceHealthCheckSuccess, - mockServiceHealthCheck, - mockStorageSecretSuccess, - mockStorageSecret, - mockCompositionConfigLinkSuccess, - mockCompositionConfigLink, - mockCompositionConfigsSuccess, - mockCompositionConfigs, - mockImplementingServicesSuccess, - mockImplementingServices, - mockRawPartialSchemaSuccess, - mockRawPartialSchema, - apiKeyHash, - graphId, -} from './nockMocks'; - -import loadServicesFromStorage = require("../../loadServicesFromStorage"); - -// This is a nice DX hack for GraphQL code highlighting and formatting within the file. -// Anything wrapped within the gql tag within this file is just a string, not an AST. -const gql = String.raw; - -export interface MockService { - gcsDefinitionPath: string; - partialSchemaPath: string; - url: string; - sdl: string; -} - -const service: MockService = { - gcsDefinitionPath: 'service-definition.json', - partialSchemaPath: 'accounts-partial-schema.json', - url: 'http://localhost:4001', - sdl: gql` - extend type Query { - me: User - everyone: [User] - } - - "This is my User" - type User @key(fields: "id") { - id: ID! - name: String - username: String - } - `, -}; - -const updatedService: MockService = { - gcsDefinitionPath: 'updated-service-definition.json', - partialSchemaPath: 'updated-accounts-partial-schema.json', - url: 'http://localhost:4002', - sdl: gql` - extend type Query { - me: User - everyone: [User] - } - - "This is my updated User" - type User @key(fields: "id") { - id: ID! - name: String - username: String - } - `, -}; - -let fetcher: typeof fetch; -let logger: Logger; - -beforeEach(() => { - if (!nock.isActive()) nock.activate(); - - fetcher = getDefaultGcsFetcher().defaults({ - retry: { - retries: GCS_RETRY_COUNT, - minTimeout: 0, - maxTimeout: 0, - }, - }); - - const warn = jest.fn(); - const debug = jest.fn(); - const error = jest.fn(); - const info = jest.fn(); - - logger = { - warn, - debug, - error, - info, - }; -}); - -afterEach(() => { - expect(nock.isDone()).toBeTruthy(); - nock.cleanAll(); - nock.restore(); -}); - -it('Queries remote endpoints for their SDLs', async () => { - mockSDLQuerySuccess(service); - - const gateway = new ApolloGateway({ - serviceList: [{ name: 'accounts', url: service.url }], - logger - }); - await gateway.load(); - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); -}); - -it('Extracts service definitions from remote storage', async () => { - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServicesSuccess(service); - mockRawPartialSchemaSuccess(service); - - const gateway = new ApolloGateway({ logger }); - - await gateway.load({ engine: { apiKeyHash, graphId } }); - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); -}); - -it.each([ - ['warned', 'present'], - ['not warned', 'absent'], -])('conflicting configurations are %s about when %s', async (_word, mode) => { - const isConflict = mode === 'present'; - let blockerResolve: () => void; - const blocker = new Promise(resolve => (blockerResolve = resolve)); - const original = loadServicesFromStorage.getServiceDefinitionsFromStorage; - const spyGetServiceDefinitionsFromStorage = jest - .spyOn(loadServicesFromStorage, 'getServiceDefinitionsFromStorage') - .mockImplementationOnce(async (...args) => { - try { - return await original(...args); - } catch (e) { - throw e; - } finally { - setImmediate(blockerResolve); - } - }); - - mockStorageSecretSuccess(); - if (isConflict) { - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServicesSuccess(service); - mockRawPartialSchemaSuccess(service); - } else { - mockCompositionConfigLink().reply(403); - } - - mockSDLQuerySuccess(service); - - const gateway = new ApolloGateway({ - serviceList: [ - { name: 'accounts', url: service.url }, - ], - logger - }); - - await gateway.load({ engine: { apiKeyHash, graphId } }); - await blocker; // Wait for the definitions to be "fetched". - - (isConflict - ? expect(logger.warn) - : expect(logger.warn).not - ).toHaveBeenCalledWith(expect.stringMatching( - /A local gateway service list is overriding an Apollo Graph Manager managed configuration/)); - spyGetServiceDefinitionsFromStorage.mockRestore(); -}); - -// This test has been flaky for a long time, and fails consistently after changes -// introduced by https://github.com/apollographql/apollo-server/pull/4277. -// I've decided to skip this test for now with hopes that we can one day -// determine the root cause and test this behavior in a reliable manner. -it.skip('Rollsback to a previous schema when triggered', async () => { - // Init - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServicesSuccess(service); - mockRawPartialSchemaSuccess(service); - - // Update 1 - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([updatedService]); - mockImplementingServicesSuccess(updatedService); - mockRawPartialSchemaSuccess(updatedService); - - // Rollback - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServices(service).reply(304); - mockRawPartialSchema(service).reply(304); - - let firstResolve: () => void; - let secondResolve: () => void; - let thirdResolve: () => void - const firstSchemaChangeBlocker = new Promise(res => (firstResolve = res)); - const secondSchemaChangeBlocker = new Promise(res => (secondResolve = res)); - const thirdSchemaChangeBlocker = new Promise(res => (thirdResolve = res)); - - const onChange = jest - .fn() - .mockImplementationOnce(() => firstResolve()) - .mockImplementationOnce(() => secondResolve()) - .mockImplementationOnce(() => thirdResolve()); - - const gateway = new ApolloGateway({ logger }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; - - gateway.onSchemaChange(onChange); - await gateway.load({ engine: { apiKeyHash, graphId } }); - - await firstSchemaChangeBlocker; - expect(onChange).toHaveBeenCalledTimes(1); - - await secondSchemaChangeBlocker; - expect(onChange).toHaveBeenCalledTimes(2); - - await thirdSchemaChangeBlocker; - expect(onChange).toHaveBeenCalledTimes(3); -}); - -function failNTimes(n: number, fn: () => nock.Interceptor) { - for (let i = 0; i < n; i++) { - fn().reply(500); - } -} - -it(`Retries GCS (up to ${GCS_RETRY_COUNT} times) on failure for each request and succeeds`, async () => { - failNTimes(GCS_RETRY_COUNT, mockStorageSecret); - mockStorageSecretSuccess(); - - failNTimes(GCS_RETRY_COUNT, mockCompositionConfigLink); - mockCompositionConfigLinkSuccess(); - - failNTimes(GCS_RETRY_COUNT, mockCompositionConfigs); - mockCompositionConfigsSuccess([service]); - - failNTimes(GCS_RETRY_COUNT, () => mockImplementingServices(service)); - mockImplementingServicesSuccess(service); - - failNTimes(GCS_RETRY_COUNT, () => mockRawPartialSchema(service)); - mockRawPartialSchemaSuccess(service); - - const gateway = new ApolloGateway({ fetcher, logger }); - - await gateway.load({ engine: { apiKeyHash, graphId } }); - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); -}); - -// This test is reliably failing in its current form. It's mostly testing that -// `make-fetch-happen` is doing its retries properly and we have proof that, -// generally speaking, retries are working, so we'll disable this until we can -// re-visit it. -it.skip(`Fails after the ${GCS_RETRY_COUNT + 1}th attempt to reach GCS`, async () => { - failNTimes(GCS_RETRY_COUNT + 1, mockStorageSecret); - - const gateway = new ApolloGateway({ fetcher, logger }); - await expect( - gateway.load({ engine: { apiKeyHash, graphId } }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Could not communicate with Apollo Graph Manager storage: "`, - ); -}); - -it(`Errors when the secret isn't hosted on GCS`, async () => { - mockStorageSecret().reply( - 403, - `AccessDenied - Anonymous caller does not have storage.objects.get`, - { 'content-type': 'application/xml' }, - ); - - const gateway = new ApolloGateway({ fetcher, logger }); - await expect( - gateway.load({ engine: { apiKeyHash, graphId } }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to authenticate with Apollo Graph Manager storage while fetching https://storage-secrets.api.apollographql.com/federated-service/storage-secret/dd55a79d467976346d229a7b12b673ce.json. Ensure that the API key is configured properly and that a federated service has been pushed. For details, see https://go.apollo.dev/g/resolve-access-denied."`, - ); -}); - -describe('Downstream service health checks', () => { - describe('Unmanaged mode', () => { - it(`Performs health checks to downstream services on load`, async () => { - mockSDLQuerySuccess(service); - mockServiceHealthCheckSuccess(service); - - const gateway = new ApolloGateway({ - logger, - serviceList: [{ name: 'accounts', url: service.url }], - serviceHealthCheck: true, - }); - - await gateway.load(); - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); - }); - - it(`Rejects on initial load when health check fails`, async () => { - mockSDLQuerySuccess(service); - mockServiceHealthCheck(service).reply(500); - - const gateway = new ApolloGateway({ - serviceList: [{ name: 'accounts', url: service.url }], - serviceHealthCheck: true, - logger, - }); - - await expect(gateway.load()).rejects.toThrowErrorMatchingInlineSnapshot( - `"500: Internal Server Error"`, - ); - }); - }); - - describe('Managed mode', () => { - it('Performs health checks to downstream services on load', async () => { - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServicesSuccess(service); - mockRawPartialSchemaSuccess(service); - - mockServiceHealthCheckSuccess(service); - - const gateway = new ApolloGateway({ serviceHealthCheck: true, logger }); - - await gateway.load({ engine: { apiKeyHash, graphId } }); - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); - }); - - it('Rejects on initial load when health check fails', async () => { - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServicesSuccess(service); - mockRawPartialSchemaSuccess(service); - - mockServiceHealthCheck(service).reply(500); - - const gateway = new ApolloGateway({ serviceHealthCheck: true, logger }); - - await expect( - gateway.load({ engine: { apiKeyHash, graphId } }), - ).rejects.toThrowErrorMatchingInlineSnapshot(`"500: Internal Server Error"`); - }); - - // This test has been flaky for a long time, and fails consistently after changes - // introduced by https://github.com/apollographql/apollo-server/pull/4277. - // I've decided to skip this test for now with hopes that we can one day - // determine the root cause and test this behavior in a reliable manner. - it.skip('Rolls over to new schema when health check succeeds', async () => { - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServicesSuccess(service); - mockRawPartialSchemaSuccess(service); - mockServiceHealthCheckSuccess(service); - - // Update - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([updatedService]); - mockImplementingServicesSuccess(updatedService); - mockRawPartialSchemaSuccess(updatedService); - mockServiceHealthCheckSuccess(updatedService); - - let resolve1: () => void; - let resolve2: () => void; - const schemaChangeBlocker1 = new Promise(res => (resolve1 = res)); - const schemaChangeBlocker2 = new Promise(res => (resolve2 = res)); - const onChange = jest - .fn() - .mockImplementationOnce(() => resolve1()) - .mockImplementationOnce(() => resolve2()); - - const gateway = new ApolloGateway({ - serviceHealthCheck: true, - logger, - }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; - - gateway.onSchemaChange(onChange); - await gateway.load({ engine: { apiKeyHash, graphId } }); - - await schemaChangeBlocker1; - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); - expect(onChange).toHaveBeenCalledTimes(1); - - await schemaChangeBlocker2; - expect(gateway.schema!.getType('User')!.description).toBe('This is my updated User'); - expect(onChange).toHaveBeenCalledTimes(2); - }); - - it('Preserves original schema when health check fails', async () => { - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([service]); - mockImplementingServicesSuccess(service); - mockRawPartialSchemaSuccess(service); - mockServiceHealthCheckSuccess(service); - - // Update - mockStorageSecretSuccess(); - mockCompositionConfigLinkSuccess(); - mockCompositionConfigsSuccess([updatedService]); - mockImplementingServicesSuccess(updatedService); - mockRawPartialSchemaSuccess(updatedService); - mockServiceHealthCheck(updatedService).reply(500); - - let resolve: () => void; - const schemaChangeBlocker = new Promise(res => (resolve = res)); - - const gateway = new ApolloGateway({ serviceHealthCheck: true, logger }); - // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here - gateway.experimental_pollInterval = 100; - - // @ts-ignore for testing purposes, we'll call the original `updateComposition` - // function from our mock. The first call should mimic original behavior, - // but the second call needs to handle the PromiseRejection. Typically for tests - // like these we would leverage the `gateway.onSchemaChange` callback to drive - // the test, but in this case, that callback isn't triggered when the update - // fails (as expected) so we get creative with the second mock as seen below. - const original = gateway.updateComposition; - const mockUpdateComposition = jest - .fn() - .mockImplementationOnce(async () => { - await original.apply(gateway); - }) - .mockImplementationOnce(async () => { - // mock the first poll and handle the error which would otherwise be caught - // and logged from within the `pollServices` class method - await expect(original.apply(gateway)) - .rejects - .toThrowErrorMatchingInlineSnapshot( - `"500: Internal Server Error"`, - ); - // finally resolve the promise which drives this test - resolve(); - }); - - // @ts-ignore for testing purposes, replace the `updateComposition` - // function on the gateway with our mock - gateway.updateComposition = mockUpdateComposition; - - // load the gateway as usual - await gateway.load({ engine: { apiKeyHash, graphId } }); - - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); - - await schemaChangeBlocker; - - // At this point, the mock update should have been called but the schema - // should not have updated to the new one. - expect(mockUpdateComposition.mock.calls.length).toBe(2); - expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); - }); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts deleted file mode 100644 index 3ad391b71f0..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts +++ /dev/null @@ -1,113 +0,0 @@ -import nock from 'nock'; -import { HEALTH_CHECK_QUERY, SERVICE_DEFINITION_QUERY } from '../..'; -import { MockService } from './networkRequests.test'; - -export const graphId = 'federated-service'; -export const apiKeyHash = 'dd55a79d467976346d229a7b12b673ce'; -const storageSecret = 'my-storage-secret'; -const accountsService = 'accounts'; - -// Service mocks -function mockSDLQuery({ url }: MockService) { - return nock(url).post('/', { - query: SERVICE_DEFINITION_QUERY, - }); -} - -export function mockSDLQuerySuccess(service: MockService) { - mockSDLQuery(service).reply(200, { - data: { _service: { sdl: service.sdl } }, - }); -} - -export function mockServiceHealthCheck({ url }: MockService) { - return nock(url).post('/', { - query: HEALTH_CHECK_QUERY, - }); -} - -export function mockServiceHealthCheckSuccess(service: MockService) { - return mockServiceHealthCheck(service).reply(200, { - data: { __typename: 'Query' }, - }); -} - -// GCS mocks -function gcsNock(url: Parameters[0]): nock.Scope { - return nock(url, { - reqheaders: { - 'user-agent': `apollo-gateway/${ - require('../../../package.json').version - }`, - }, - }); -} - -export function mockStorageSecret() { - return gcsNock('https://storage-secrets.api.apollographql.com:443').get( - `/${graphId}/storage-secret/${apiKeyHash}.json`, - ); -} - -export function mockStorageSecretSuccess() { - return gcsNock('https://storage-secrets.api.apollographql.com:443') - .get( - `/${graphId}/storage-secret/${apiKeyHash}.json`, - ) - .reply(200, `"${storageSecret}"`); -} - -// get composition config link, using received storage secret -export function mockCompositionConfigLink() { - return gcsNock('https://federation.api.apollographql.com:443').get( - `/${storageSecret}/current/v1/composition-config-link`, - ); -} - -export function mockCompositionConfigLinkSuccess() { - return mockCompositionConfigLink().reply(200, { - configPath: `${storageSecret}/current/v1/composition-configs/composition-config-path.json`, - }); -} - -// get composition configs, using received composition config link -export function mockCompositionConfigs() { - return gcsNock('https://federation.api.apollographql.com:443').get( - `/${storageSecret}/current/v1/composition-configs/composition-config-path.json`, - ); -} - -export function mockCompositionConfigsSuccess(services: MockService[]) { - return mockCompositionConfigs().reply(200, { - implementingServiceLocations: services.map(service => ({ - name: accountsService, - path: `${storageSecret}/current/v1/implementing-services/${accountsService}/${service.gcsDefinitionPath}`, - })), - }); -} - -// get implementing service reference, using received composition-config -export function mockImplementingServices({ gcsDefinitionPath }: MockService) { - return gcsNock('https://federation.api.apollographql.com:443').get( - `/${storageSecret}/current/v1/implementing-services/${accountsService}/${gcsDefinitionPath}`, - ); -} - -export function mockImplementingServicesSuccess(service: MockService) { - return mockImplementingServices(service).reply(200, { - name: accountsService, - partialSchemaPath: `${storageSecret}/current/raw-partial-schemas/${service.partialSchemaPath}`, - url: service.url, - }); -} - -// get raw-partial-schema, using received composition-config -export function mockRawPartialSchema({ partialSchemaPath }: MockService) { - return gcsNock('https://federation.api.apollographql.com:443').get( - `/${storageSecret}/current/raw-partial-schemas/${partialSchemaPath}`, - ); -} - -export function mockRawPartialSchemaSuccess(service: MockService) { - return mockRawPartialSchema(service).reply(200, service.sdl); -} diff --git a/packages/apollo-gateway/src/__tests__/integration/provides.test.ts b/packages/apollo-gateway/src/__tests__/integration/provides.test.ts deleted file mode 100644 index 14c92513986..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/provides.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { execute, overrideResolversInService } from '../execution-utils'; -import { fixtures } from 'apollo-federation-integration-testsuite'; - -it('does not have to go to another service when field is given', async () => { - const query = `#graphql - query GetReviewers { - topReviews { - author { - username - } - } - } - `; - - const { data, queryPlan } = await execute( { - query, - }); - - expect(data).toEqual({ - topReviews: [ - { author: { username: '@ada' } }, - { author: { username: '@ada' } }, - { author: { username: '@complete' } }, - { author: { username: '@complete' } }, - { author: { username: '@complete' } }, - ], - }); - - expect(queryPlan).not.toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); -}); - -it('does not load fields provided even when going to other service', async () => { - const [accounts, ...restFixtures] = fixtures; - - const username = jest.fn(); - const localAccounts = overrideResolversInService(accounts, { - User: { - username, - }, - }); - - const query = `#graphql - query GetReviewers { - topReviews { - author { - username - name { - first - last - } - } - } - } - `; - - const { data, queryPlan } = await execute( - { - query, - }, - [localAccounts, ...restFixtures], - ); - - expect(data).toEqual({ - topReviews: [ - { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, - { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, - ], - }); - - expect(username).not.toHaveBeenCalled(); - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/requires.test.ts b/packages/apollo-gateway/src/__tests__/integration/requires.test.ts deleted file mode 100644 index 1ea07cce2b4..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/requires.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -import gql from 'graphql-tag'; -import { execute } from '../execution-utils'; -import { serializeQueryPlan } from '../..'; - -it('supports passing additional fields defined by a requires', async () => { - const query = `#graphql - query GetReviwedBookNames { - me { - reviews { - product { - ... on Book { - name - } - } - } - } - } - `; - - const { data, queryPlan } = await execute({ - query, - }); - - expect(data).toEqual({ - me: { - reviews: [ - { product: {} }, - { product: {} }, - { - product: { - name: 'Design Patterns (1995)', - }, - }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); - expect(queryPlan).toCallService('product'); - expect(queryPlan).toCallService('books'); -}); - -const serviceA = { - name: 'a', - typeDefs: gql` - type Query { - user: User - } - - type User @key(fields: "id") { - id: ID! - preferences: Preferences - } - - type Preferences { - favorites: Things - } - - type Things { - color: String - animal: String - } - `, - resolvers: { - Query: { - user() { - return { - id: '1', - preferences: { - favorites: { color: 'limegreen', animal: 'platypus' }, - }, - }; - }, - }, - }, -}; - -const serviceB = { - name: 'b', - typeDefs: gql` - extend type User @key(fields: "id") { - id: ID! @external - preferences: Preferences @external - favoriteColor: String - @requires(fields: "preferences { favorites { color } }") - favoriteAnimal: String - @requires(fields: "preferences { favorites { animal } }") - } - - extend type Preferences { - favorites: Things @external - } - - extend type Things { - color: String @external - animal: String @external - } - `, - resolvers: { - User: { - favoriteColor(user: any) { - return user.preferences.favorites.color; - }, - favoriteAnimal(user: any) { - return user.preferences.favorites.animal; - }, - }, - }, -}; - -it('collapses nested requires', async () => { - const query = `#graphql - query UserFavorites { - user { - favoriteColor - favoriteAnimal - } - } - `; - - const { data, errors, queryPlan } = await execute( - { - query, - }, - [serviceA, serviceB], - ); - - expect(errors).toEqual(undefined); - - expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(` - "QueryPlan { - Sequence { - Fetch(service: \\"a\\") { - { - user { - __typename - id - preferences { - favorites { - color - animal - } - } - } - } - }, - Flatten(path: \\"user\\") { - Fetch(service: \\"b\\") { - { - ... on User { - __typename - id - preferences { - favorites { - color - animal - } - } - } - } => - { - ... on User { - favoriteColor - favoriteAnimal - } - } - }, - }, - }, - }" - `); - - expect(data).toEqual({ - user: { - favoriteAnimal: 'platypus', - favoriteColor: 'limegreen', - }, - }); - - expect(queryPlan).toCallService('a'); - expect(queryPlan).toCallService('b'); -}); - -it('collapses nested requires with user-defined fragments', async () => { - const query = `#graphql - query UserFavorites { - user { - favoriteAnimal - ...favoriteColor - } - } - - fragment favoriteColor on User { - preferences { - favorites { - color - } - } - } - `; - - const { data, errors, queryPlan } = await execute( - { - query, - }, - [serviceA, serviceB], - ); - - expect(errors).toEqual(undefined); - - expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(` - "QueryPlan { - Sequence { - Fetch(service: \\"a\\") { - { - user { - __typename - id - preferences { - favorites { - animal - color - } - } - } - } - }, - Flatten(path: \\"user\\") { - Fetch(service: \\"b\\") { - { - ... on User { - __typename - id - preferences { - favorites { - animal - } - } - } - } => - { - ... on User { - favoriteAnimal - } - } - }, - }, - }, - }" - `); - - expect(data).toEqual({ - user: { - favoriteAnimal: 'platypus', - preferences: { - favorites: { - color: 'limegreen', - }, - }, - }, - }); - - expect(queryPlan).toCallService('a'); - expect(queryPlan).toCallService('b'); -}); - -it('passes null values correctly', async () => { - const serviceA = { - name: 'a', - typeDefs: gql` - type Query { - user: User - } - - type User @key(fields: "id") { - id: ID! - favorite: Color - dislikes: [Color] - } - - type Color { - name: String! - } - `, - resolvers: { - Query: { - user() { - return { - id: '1', - favorite: null, - dislikes: [null], - }; - }, - }, - }, - }; - - const serviceB = { - name: 'b', - typeDefs: gql` - extend type User @key(fields: "id") { - id: ID! @external - favorite: Color @external - dislikes: [Color] @external - favoriteColor: String @requires(fields: "favorite { name }") - dislikedColors: String @requires(fields: "dislikes { name }") - } - - extend type Color { - name: String! @external - } - `, - resolvers: { - User: { - favoriteColor(user: any) { - if (user.favorite !== null) { - throw Error( - 'Favorite color should be null. Instead, got: ' + - JSON.stringify(user.favorite), - ); - } - return 'unknown'; - }, - dislikedColors(user: any) { - const color = user.dislikes[0]; - if (color !== null) { - throw Error( - 'Disliked colors should be null. Instead, got: ' + - JSON.stringify(user.dislikes), - ); - } - return 'unknown'; - }, - }, - }, - }; - - const query = `#graphql - query UserFavorites { - user { - favoriteColor - dislikedColors - } - } - `; - - const { data, errors } = await execute({ query }, [serviceA, serviceB]); - - expect(errors).toEqual(undefined); - expect(data).toEqual({ - user: { - favoriteColor: 'unknown', - dislikedColors: 'unknown', - }, - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/single-service.test.ts b/packages/apollo-gateway/src/__tests__/integration/single-service.test.ts deleted file mode 100644 index 53ab513b0a6..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/single-service.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import gql from 'graphql-tag'; -import { execute, overrideResolversInService } from '../execution-utils'; - -const accounts = { - name: 'accounts', - typeDefs: gql` - type User @key(fields: "id") { - id: Int! - name: String - account: Account - } - type Account { - type: String - } - extend type Query { - me: User - } - `, - resolvers: { - Query: { - me: () => ({ id: 1, name: 'Martijn' }), - }, - }, -}; - -it('executes a query plan over concrete types', async () => { - const me = jest.fn(() => ({ id: 1, name: 'James' })); - const localAccounts = overrideResolversInService(accounts, { - Query: { me }, - }); - - const query = `#graphql - query GetUser { - me { - id - name - } - } - `; - const { data, queryPlan } = await execute( - { - query, - }, - [localAccounts], - ); - - expect(data).toEqual({ me: { id: 1, name: 'James' } }); - expect(queryPlan).toCallService('accounts'); - expect(me).toBeCalled(); -}); - -it('does not remove __typename on root types', async () => { - const query = `#graphql - query GetUser { - __typename - } - `; - - const { data } = await execute( - { - query, - }, - [accounts], - ); - - expect(data).toEqual({ __typename: 'Query' }); -}); - -it('does not remove __typename if that is all that is requested on an entity', async () => { - const me = jest.fn(() => ({ id: 1, name: 'James' })); - const localAccounts = overrideResolversInService(accounts, { - Query: { me }, - }); - - const query = `#graphql - query GetUser { - me { - __typename - } - } - `; - const { data, queryPlan } = await execute( - { - query, - }, - [localAccounts], - ); - - expect(data).toEqual({ me: { __typename: 'User' } }); - expect(queryPlan).toCallService('accounts'); - expect(me).toBeCalled(); -}); - -it('does not remove __typename if that is all that is requested on a value type', async () => { - const me = jest.fn(() => ({ id: 1, name: 'James', account: {} })); - const localAccounts = overrideResolversInService(accounts, { - Query: { me }, - }); - - const query = `#graphql - query GetUser { - me { - account { - __typename - } - } - } - `; - const { data, queryPlan } = await execute( - { - query, - }, - [localAccounts], - ); - - expect(data).toEqual({ me: { account: { __typename: 'Account' } } }); - expect(queryPlan).toCallService('accounts'); - expect(me).toBeCalled(); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/unions.test.ts b/packages/apollo-gateway/src/__tests__/integration/unions.test.ts deleted file mode 100644 index 42827bb3559..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/unions.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import gql from 'graphql-tag'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; -import { execute } from '../execution-utils'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -it('handles multiple union type conditions that share a response name (media)', async () => { - const query = `#graphql - query { - content { - ...Audio - ... on Video { - media { - aspectRatio - } - } - } - } - fragment Audio on Audio { - media { - url - } - } - `; - - const { queryPlan, errors } = await execute( - { query }, - [ - { - name: 'contentService', - typeDefs: gql` - extend type Query { - content: Content - } - union Content = Audio | Video - type Audio { - media: AudioURL - } - type AudioURL { - url: String - } - type Video { - media: VideoAspectRatio - } - type VideoAspectRatio { - aspectRatio: String - } - `, - resolvers: { - Query: {}, - }, - }, - ], - ); - - expect(errors).toBeUndefined(); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Fetch(service: "contentService") { - { - content { - __typename - ... on Audio { - media { - url - } - } - ... on Video { - media { - aspectRatio - } - } - } - } - }, - } - `); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/value-types.test.ts b/packages/apollo-gateway/src/__tests__/integration/value-types.test.ts deleted file mode 100644 index 58064ce1731..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/value-types.test.ts +++ /dev/null @@ -1,382 +0,0 @@ -import gql from 'graphql-tag'; -import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; - -expect.addSnapshotSerializer(astSerializer); -expect.addSnapshotSerializer(queryPlanSerializer); - -describe('value types', () => { - it('resolves value types within their respective services', async () => { - const query = `#graphql - fragment Metadata on MetadataOrError { - ... on KeyValue { - key - value - } - ... on Error { - code - message - } - } - - query ProducsWithMetadata { - topProducts(first: 10) { - upc - ... on Book { - metadata { - ...Metadata - } - } - ... on Furniture { - metadata { - ...Metadata - } - } - reviews { - metadata { - ...Metadata - } - } - } - } - `; - - const { data, errors, queryPlan } = await execute({ - query, - }); - - expect(errors).toBeUndefined(); - - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "product") { - { - topProducts(first: 10) { - __typename - ... on Book { - upc - __typename - isbn - } - ... on Furniture { - upc - metadata { - __typename - ... on KeyValue { - key - value - } - ... on Error { - code - message - } - } - __typename - } - } - } - }, - Parallel { - Flatten(path: "topProducts.@") { - Fetch(service: "books") { - { - ... on Book { - __typename - isbn - } - } => - { - ... on Book { - metadata { - __typename - ... on KeyValue { - key - value - } - ... on Error { - code - message - } - } - } - } - }, - }, - Flatten(path: "topProducts.@") { - Fetch(service: "reviews") { - { - ... on Book { - __typename - isbn - } - ... on Furniture { - __typename - upc - } - } => - { - ... on Book { - reviews { - metadata { - __typename - ... on KeyValue { - key - value - } - ... on Error { - code - message - } - } - } - } - ... on Furniture { - reviews { - metadata { - __typename - ... on KeyValue { - key - value - } - ... on Error { - code - message - } - } - } - } - } - }, - }, - }, - }, - } - `); - - const [furniture, , , , book] = data!.topProducts; - - // Sanity check, referenceable ID - expect(furniture.upc).toEqual('1'); - // Value type resolves from the correct service - expect(furniture.metadata[0]).toEqual({ - key: 'Condition', - value: 'excellent', - }); - - // Value type from a different service (reviews) also resolves correctly - expect(furniture.reviews[0].metadata[0]).toEqual({ - code: 418, - message: "I'm a teapot", - }); - - // Sanity check, referenceable ID - expect(book.upc).toEqual('0136291554'); - // Value type as a union resolves correctly - expect(book.metadata).toEqual([ - { - key: 'Condition', - value: 'used', - }, - { - code: 401, - message: 'Unauthorized', - }, - ]); - - expect(queryPlan).toCallService('product'); - expect(queryPlan).toCallService('books'); - expect(queryPlan).toCallService('reviews'); - }); - - it('resolves @provides fields on value types correctly via contrived example', async () => { - const firstService = { - name: 'firstService', - typeDefs: gql` - extend type Query { - valueType: ValueType - } - - type ValueType { - id: ID! - user: User! @provides(fields: "id name") - } - - extend type User @key(fields: "id") { - id: ID! @external - name: String! @external - } - `, - resolvers: { - Query: { - valueType() { - return { id: '123', user: { id: '1', name: 'trevor' } }; - }, - }, - }, - }; - - const secondService = { - name: 'secondService', - typeDefs: gql` - extend type Query { - otherValueType: ValueType - } - - type ValueType { - id: ID! - user: User! @provides(fields: "id name") - } - - extend type User @key(fields: "id") { - id: ID! @external - name: String! @external - } - `, - resolvers: { - Query: { - otherValueType() { - return { id: '456', user: { id: '2', name: 'james' } }; - }, - }, - }, - }; - - const userService = { - name: 'userService', - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - name: String! - address: String! - } - `, - resolvers: { - User: { - __resolveReference(user: any) { - return user.id === '1' - ? { id: '1', name: 'trevor', address: '123 Abc St' } - : { id: '2', name: 'james', address: '456 Hello St.' }; - }, - }, - }, - }; - - const query = `#graphql - query Hello { - valueType { - id - user { - id - name - address - } - } - otherValueType { - id - user { - id - name - address - } - } - } - `; - - const { data, errors, queryPlan } = await execute( - { - query, - }, - [firstService, secondService, userService], - ); - - expect(errors).toBeUndefined(); - expect(queryPlan).toCallService('firstService'); - expect(queryPlan).toCallService('secondService'); - expect(queryPlan).toCallService('userService'); - expect(data).toMatchInlineSnapshot(` - Object { - "otherValueType": Object { - "id": "456", - "user": Object { - "address": "456 Hello St.", - "id": "2", - "name": "james", - }, - }, - "valueType": Object { - "id": "123", - "user": Object { - "address": "123 Abc St", - "id": "1", - "name": "trevor", - }, - }, - } - `); - expect(queryPlan).toMatchInlineSnapshot(` - QueryPlan { - Parallel { - Sequence { - Fetch(service: "firstService") { - { - valueType { - id - user { - id - name - __typename - } - } - } - }, - Flatten(path: "valueType.user") { - Fetch(service: "userService") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - address - } - } - }, - }, - }, - Sequence { - Fetch(service: "secondService") { - { - otherValueType { - id - user { - id - name - __typename - } - } - } - }, - Flatten(path: "otherValueType.user") { - Fetch(service: "userService") { - { - ... on User { - __typename - id - } - } => - { - ... on User { - address - } - } - }, - }, - }, - }, - } - `); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/integration/variables.test.ts b/packages/apollo-gateway/src/__tests__/integration/variables.test.ts deleted file mode 100644 index 15209f44f5c..00000000000 --- a/packages/apollo-gateway/src/__tests__/integration/variables.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { execute } from '../execution-utils'; - -it('passes variables to root fields', async () => { - const query = `#graphql - query GetProduct($upc: String!) { - product(upc: $upc) { - name - } - } - `; - - const upc = '1'; - const { data, errors, queryPlan } = await execute({ - query, - variables: { upc }, - }); - - expect(errors).toBeUndefined(); - expect(data).toEqual({ - product: { - name: 'Table', - }, - }); - - expect(queryPlan).toCallService('product'); -}); - -it('supports default variables in a variable definition', async () => { - const query = `#graphql - query GetProduct($upc: String = "1") { - product(upc: $upc) { - name - } - } - `; - - const { data, errors, queryPlan } = await execute({ - query, - }); - - expect(errors).toBeUndefined(); - expect(data).toEqual({ - product: { - name: 'Table', - }, - }); - - expect(queryPlan).toCallService('product'); -}); - -it('passes variables to nested services', async () => { - const query = `#graphql - query GetProductsForUser($format: Boolean) { - me { - reviews { - body(format: $format) - } - } - } - `; - - const format = true; - const { data, errors, queryPlan } = await execute({ - query, - variables: { format }, - }); - - expect(errors).toBeUndefined(); - expect(data).toEqual({ - me: { - reviews: [ - { body: 'Love it!' }, - { body: 'Too expensive.' }, - { - body: 'A classic.', - }, - ], - }, - }); - - expect(queryPlan).toCallService('accounts'); - expect(queryPlan).toCallService('reviews'); -}); - -it('works with default variables in the schema', async () => { - const query = `#graphql - query LibraryUser($libraryId: ID!, $userId: ID) { - library(id: $libraryId) { - userAccount(id: $userId) { - id - name { - first - last - } - } - } - } - `; - - const { data, queryPlan, errors } = await execute({ - query, - variables: { libraryId: '1' }, - }); - - expect(data).toEqual({ - library: { - userAccount: { - id: '1', - name: { - first: 'Ada', - last: 'Lovelace', - } - }, - }, - }); - - expect(errors).toBeUndefined(); - expect(queryPlan).toCallService('books'); - expect(queryPlan).toCallService('accounts'); -}); diff --git a/packages/apollo-gateway/src/__tests__/loadServicesFromRemoteEndpoint.test.ts b/packages/apollo-gateway/src/__tests__/loadServicesFromRemoteEndpoint.test.ts deleted file mode 100644 index 85d89c12575..00000000000 --- a/packages/apollo-gateway/src/__tests__/loadServicesFromRemoteEndpoint.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getServiceDefinitionsFromRemoteEndpoint } from '../loadServicesFromRemoteEndpoint'; -import { RemoteGraphQLDataSource } from '../datasources'; - -describe('getServiceDefinitionsFromRemoteEndpoint', () => { - it('errors when no URL was specified', async () => { - const serviceSdlCache = new Map(); - const dataSource = new RemoteGraphQLDataSource({ url: '' }); - const serviceList = [{ name: 'test', dataSource }]; - await expect( - getServiceDefinitionsFromRemoteEndpoint({ - serviceList, - serviceSdlCache, - }), - ).rejects.toThrowError( - "Tried to load schema for 'test' but no 'url' was specified.", - ); - }); - - it('throws when the downstream service returns errors', async () => { - const serviceSdlCache = new Map(); - const host = 'http://host-which-better-not-resolve'; - const url = host + '/graphql'; - - const dataSource = new RemoteGraphQLDataSource({ url }); - const serviceList = [{ name: 'test', url, dataSource }]; - // Depending on the OS's resolver, the error may result in an error - // of `EAI_AGAIN` or `ENOTFOUND`. This `toThrowError` uses a Regex - // to match either case. - await expect( - getServiceDefinitionsFromRemoteEndpoint({ - serviceList, - serviceSdlCache, - }), - ).rejects.toThrowError(/^Couldn't load service definitions for "test" at http:\/\/host-which-better-not-resolve\/graphql: request to http:\/\/host-which-better-not-resolve\/graphql failed, reason: getaddrinfo (ENOTFOUND|EAI_AGAIN)/); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/matchers/toCallService.ts b/packages/apollo-gateway/src/__tests__/matchers/toCallService.ts deleted file mode 100644 index 1640861fa62..00000000000 --- a/packages/apollo-gateway/src/__tests__/matchers/toCallService.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { QueryPlan } from '@apollo/gateway'; -import { PlanNode } from '../../QueryPlan'; -import astSerializer from '../../snapshotSerializers/astSerializer'; -import queryPlanSerializer from '../../snapshotSerializers/queryPlanSerializer'; -const prettyFormat = require('pretty-format'); - -declare global { - namespace jest { - interface Matchers { - toCallService(service: string): R; - } - } -} - -// function printNode(node: ExecutionNode) { -// return prettyFormat( -// { nodes: [node], kind: 'QueryPlan' }, -// { -// plugins: [queryPlanSerializer, astSerializer], -// }, -// ); -// } - -const lineEndRegex = /^/gm; -function indentString(string: string, count = 2) { - if (!string) return string; - return string.replace(lineEndRegex, ' '.repeat(count)); -} - -function toCallService( - this: jest.MatcherUtils, - queryPlan: QueryPlan, - service: string, -): { message(): string; pass: boolean } { - // const receivedString = print(received); - // const expectedString = print(expected); - - const printReceived = (string: string) => - this.utils.RECEIVED_COLOR(indentString(string)); - const printExpected = (string: string) => - this.utils.EXPECTED_COLOR(indentString(string)); - - let pass = false; - // let initialServiceCall = null; - // recurse the node, find first match of service name, return - function walkExecutionNode(node?: PlanNode) { - if (!node) return; - if (node.kind === 'Fetch' && node.serviceName === service) { - pass = true; - // initialServiceCall = node; - return; - } - switch (node.kind) { - case 'Flatten': - walkExecutionNode(node.node); - break; - case 'Parallel': - case 'Sequence': - node.nodes.forEach(walkExecutionNode); - break; - default: - return; - } - } - - walkExecutionNode(queryPlan.node); - - const message = pass - ? () => - this.utils.matcherHint('.not.toCallService') + - '\n\n' + - `Expected query plan to not call service:\n` + - printExpected(service) + - '\n' + - `Received:\n` + - // FIXME print just the node - printReceived( - prettyFormat(queryPlan, { - plugins: [queryPlanSerializer, astSerializer], - }), - ) - : () => { - return ( - this.utils.matcherHint('.toCallService') + - '\n\n' + - `Expected query plan to call service:\n` + - printExpected(service) + - '\n' + - `Received query plan:\n` + - printReceived( - prettyFormat(queryPlan, { - plugins: [queryPlanSerializer, astSerializer], - }), - ) - ); - }; - return { - message, - pass, - }; -} - -expect.extend({ - toCallService, -}); diff --git a/packages/apollo-gateway/src/__tests__/matchers/toHaveBeenCalledBefore.ts b/packages/apollo-gateway/src/__tests__/matchers/toHaveBeenCalledBefore.ts deleted file mode 100644 index 1b5f86c64f3..00000000000 --- a/packages/apollo-gateway/src/__tests__/matchers/toHaveBeenCalledBefore.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Make this file a module -// See: https://github.com/microsoft/TypeScript/issues/17736 -export {}; -declare global { - namespace jest { - interface Matchers { - toHaveBeenCalledBefore(spy: SpyInstance): R; - } - } -} - -function toHaveBeenCalledBefore( - this: jest.MatcherUtils, - firstSpy: jest.SpyInstance, - secondSpy: jest.SpyInstance, -): { message(): string; pass: boolean } { - const firstSpyEarliestCall = Math.min(...firstSpy.mock.invocationCallOrder); - const secondSpyEarliestCall = Math.min(...secondSpy.mock.invocationCallOrder); - - const pass = firstSpyEarliestCall < secondSpyEarliestCall; - - const message = pass - ? () => - this.utils.matcherHint('.not.toHaveBeenCalledBefore') + - '\n\n' + - `Expected ${firstSpy.getMockName()} not to have been called before ${secondSpy.getMockName()}` - : () => - this.utils.matcherHint('.toHaveBeenCalledBefore') + - '\n\n' + - `Expected ${firstSpy.getMockName()} to have been called before ${secondSpy.getMockName()}`; - - return { - message, - pass, - }; -} - -expect.extend({ - toHaveBeenCalledBefore, -}); diff --git a/packages/apollo-gateway/src/__tests__/matchers/toHaveFetched.ts b/packages/apollo-gateway/src/__tests__/matchers/toHaveFetched.ts deleted file mode 100644 index 4ebbcc24ba7..00000000000 --- a/packages/apollo-gateway/src/__tests__/matchers/toHaveFetched.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Request, RequestInit, Headers } from 'apollo-server-env'; - -// Make this file a module -// See: https://github.com/microsoft/TypeScript/issues/17736 -export {}; -declare global { - namespace jest { - interface Matchers { - toHaveFetched(spy: SpyInstance): R; - } - } -} - -type ExtendedRequest = RequestInit & { url: string }; - -function prepareHttpRequest(request: ExtendedRequest): Request { - const headers = new Headers(); - headers.set('Content-Type', 'application/json'); - if (request.headers) { - for (let name in request.headers) { - headers.set(name, request.headers[name]); - } - } - - const options: RequestInit = { - method: 'POST', - headers, - body: JSON.stringify(request.body), - }; - - return new Request(request.url, options); -} - -function toHaveFetched( - this: jest.MatcherUtils, - fetch: jest.SpyInstance, - request: ExtendedRequest, -): { message(): string; pass: boolean } { - const httpRequest = prepareHttpRequest(request); - let pass = false; - let message = () => ''; - try { - expect(fetch).toBeCalledWith(httpRequest); - pass = true; - } catch (e) { - message = () => e.message; - } - - return { - message, - pass, - }; -} - -function toHaveFetchedNth( - this: jest.MatcherUtils, - fetch: jest.SpyInstance, - nthCall: number, - request: ExtendedRequest, -): { message(): string; pass: boolean } { - const httpRequest = prepareHttpRequest(request); - let pass = false; - let message = () => ''; - try { - expect(fetch).toHaveBeenNthCalledWith(nthCall, httpRequest); - pass = true; - } catch (e) { - message = () => e.message; - } - - return { - message, - pass, - }; -} - - -expect.extend({ - toHaveFetched, - toHaveFetchedNth, -}); diff --git a/packages/apollo-gateway/src/__tests__/matchers/toMatchAST.ts b/packages/apollo-gateway/src/__tests__/matchers/toMatchAST.ts deleted file mode 100644 index 8df057e7c96..00000000000 --- a/packages/apollo-gateway/src/__tests__/matchers/toMatchAST.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { print, ASTNode } from 'graphql'; -const diff = require('jest-diff'); - -declare global { - namespace jest { - interface Matchers { - toMatchAST(expected: ASTNode): R; - } - } -} - -const lineEndRegex = /^/gm; -function indentString(string: string, count = 2) { - if (!string) return string; - return string.replace(lineEndRegex, ' '.repeat(count)); -} - -function toMatchAST( - this: jest.MatcherUtils, - received: ASTNode, - expected: ASTNode, -): { message(): string; pass: boolean } { - const receivedString = print(received); - const expectedString = print(expected); - - const printReceived = (string: string) => - this.utils.RECEIVED_COLOR(indentString(string)); - const printExpected = (string: string) => - this.utils.EXPECTED_COLOR(indentString(string)); - - const pass = this.equals(receivedString, expectedString); - const message = pass - ? () => - this.utils.matcherHint('.not.toMatchAST') + - '\n\n' + - `Expected AST to not equal:\n` + - printExpected(expectedString) + - '\n' + - `Received:\n` + - printReceived(receivedString) - : () => { - const diffString = diff(expectedString, receivedString, { - expand: this.expand, - }); - return ( - this.utils.matcherHint('.toMatchAST') + - '\n\n' + - `Expected AST to equal:\n` + - printExpected(expectedString) + - '\n' + - `Received:\n` + - printReceived(receivedString) + - (diffString ? `\n\nDifference:\n\n${diffString}` : '') - ); - }; - return { - message, - pass, - }; -} - -expect.extend({ - toMatchAST, -}); diff --git a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts deleted file mode 100644 index 120e3a68179..00000000000 --- a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import gql from 'graphql-tag'; -import { GraphQLSchemaValidationError } from 'apollo-graphql'; -import { defineFeature, loadFeature } from 'jest-cucumber'; -import { DocumentNode, GraphQLSchema, GraphQLError, Kind } from 'graphql'; - -import { QueryPlan } from '../..'; -import { buildQueryPlan, buildOperationContext, BuildQueryPlanOptions } from '../buildQueryPlan'; -import { getFederatedTestingSchema } from './execution-utils'; - -const buildQueryPlanFeature = loadFeature( - './packages/apollo-gateway/src/__tests__/build-query-plan.feature' -); - - -const features = [ - buildQueryPlanFeature -]; - -features.forEach((feature) => { - defineFeature(feature, (test) => { - feature.scenarios.forEach((scenario) => { - test(scenario.title, async ({ given, when, then }) => { - let query: DocumentNode; - let queryPlan: QueryPlan; - let options: BuildQueryPlanOptions = { autoFragmentization: false }; - - const { schema, errors } = getFederatedTestingSchema(); - - if (errors && errors.length > 0) { - throw new GraphQLSchemaValidationError(errors); - } - - const givenQuery = () => { - given(/^query$/im, (operation: string) => { - query = gql(operation) - }) - } - - const whenUsingAutoFragmentization = () => { - when(/using autofragmentization/i, () => { - options = { autoFragmentization: true }; - }) - } - - const thenQueryPlanShouldBe = () => { - then(/^query plan$/i, (expectedQueryPlan: string) => { - queryPlan = buildQueryPlan( - buildOperationContext(schema, query, undefined), - options - ); - - const parsedExpectedPlan = JSON.parse(expectedQueryPlan); - - expect(queryPlan).toEqual(parsedExpectedPlan); - }) - } - - // step over each defined step in the .feature and execute the correct - // matching step fn defined above - scenario.steps.forEach(({ stepText }) => { - const title = stepText.toLocaleLowerCase(); - if (title === "query") givenQuery(); - else if (title === "using autofragmentization") whenUsingAutoFragmentization(); - else if (title === "query plan") thenQueryPlanShouldBe(); - else throw new Error(`Unrecognized steps used in "build-query-plan.feature"`); - }); - }); - }); - }); -}); diff --git a/packages/apollo-gateway/src/__tests__/testSetup.ts b/packages/apollo-gateway/src/__tests__/testSetup.ts deleted file mode 100644 index eab33782f0a..00000000000 --- a/packages/apollo-gateway/src/__tests__/testSetup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './matchers/toMatchAST'; -import './matchers/toCallService'; -import './matchers/toHaveBeenCalledBefore'; -import './matchers/toHaveFetched'; diff --git a/packages/apollo-gateway/src/__tests__/tsconfig.json b/packages/apollo-gateway/src/__tests__/tsconfig.json deleted file mode 100644 index d7cd9b716cc..00000000000 --- a/packages/apollo-gateway/src/__tests__/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [ - { "path": "../../" }, - ] -} diff --git a/packages/apollo-gateway/src/buildQueryPlan.ts b/packages/apollo-gateway/src/buildQueryPlan.ts deleted file mode 100644 index ac528f5a340..00000000000 --- a/packages/apollo-gateway/src/buildQueryPlan.ts +++ /dev/null @@ -1,1191 +0,0 @@ -import { isNotNullOrUndefined } from 'apollo-env'; -import { - DocumentNode, - FieldNode, - FragmentDefinitionNode, - getNamedType, - getOperationRootType, - GraphQLAbstractType, - GraphQLCompositeType, - GraphQLError, - GraphQLField, - GraphQLObjectType, - GraphQLSchema, - GraphQLType, - InlineFragmentNode, - isAbstractType, - isCompositeType, - isIntrospectionType, - isListType, - isNamedType, - isObjectType, - Kind, - OperationDefinitionNode, - SelectionSetNode, - typeFromAST, - TypeNameMetaFieldDef, - visit, - VariableDefinitionNode, - OperationTypeNode, - print, - stripIgnoredCharacters, -} from 'graphql'; -import { - Field, - FieldSet, - groupByParentType, - groupByResponseName, - matchesField, - selectionSetFromFieldSet, - Scope, -} from './FieldSet'; -import { - FetchNode, - ParallelNode, - PlanNode, - SequenceNode, - QueryPlan, - ResponsePath, - OperationContext, - trimSelectionNodes, - FragmentMap, -} from './QueryPlan'; -import { getFieldDef, getResponseName } from './utilities/graphql'; -import { MultiMap } from './utilities/MultiMap'; -import { getFederationMetadata } from '@apollo/federation/dist/composition/utils'; - -const typenameField = { - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: TypeNameMetaFieldDef.name, - }, -}; - -export interface BuildQueryPlanOptions { - autoFragmentization: boolean; -} - -export function buildQueryPlan( - operationContext: OperationContext, - options: BuildQueryPlanOptions = { autoFragmentization: false }, -): QueryPlan { - const context = buildQueryPlanningContext(operationContext, options); - - if (context.operation.operation === 'subscription') { - throw new GraphQLError( - 'Query planning does not support subscriptions for now.', - [context.operation], - ); - } - - const rootType = getOperationRootType(context.schema, context.operation); - - const isMutation = context.operation.operation === 'mutation'; - - const fields = collectFields( - context, - context.newScope(rootType), - context.operation.selectionSet, - ); - - // Mutations are a bit more specific in how FetchGroups can be built, as some - // calls to the same service may need to be executed serially. - const groups = isMutation - ? splitRootFieldsSerially(context, fields) - : splitRootFields(context, fields); - - const nodes = groups.map(group => - executionNodeForGroup(context, group, rootType), - ); - - return { - kind: 'QueryPlan', - node: nodes.length - // if an operation is a mutation, we run the root fields in sequence, - // otherwise we run them in parallel - ? flatWrap(isMutation ? 'Sequence' : 'Parallel', nodes) - : undefined, - }; -} - -function executionNodeForGroup( - context: QueryPlanningContext, - { - serviceName, - fields, - requiredFields, - internalFragments, - mergeAt, - dependentGroups, - }: FetchGroup, - parentType?: GraphQLCompositeType, -): PlanNode { - const selectionSet = selectionSetFromFieldSet(fields, parentType); - const requires = - requiredFields.length > 0 - ? selectionSetFromFieldSet(requiredFields) - : undefined; - const variableUsages = context.getVariableUsages( - selectionSet, - internalFragments, - ); - - const operation = requires - ? operationForEntitiesFetch({ - selectionSet, - variableUsages, - internalFragments, - }) - : operationForRootFetch({ - selectionSet, - variableUsages, - internalFragments, - operation: context.operation.operation, - }); - - const fetchNode: FetchNode = { - kind: 'Fetch', - serviceName, - requires: requires ? trimSelectionNodes(requires?.selections) : undefined, - variableUsages: Object.keys(variableUsages), - operation: stripIgnoredCharacters(print(operation)), - }; - - const node: PlanNode = - mergeAt && mergeAt.length > 0 - ? { - kind: 'Flatten', - path: mergeAt, - node: fetchNode, - } - : fetchNode; - - if (dependentGroups.length > 0) { - const dependentNodes = dependentGroups.map(dependentGroup => - executionNodeForGroup(context, dependentGroup), - ); - - return flatWrap('Sequence', [node, flatWrap('Parallel', dependentNodes)]); - } else { - return node; - } -} - -interface VariableUsages { - [name: string]: VariableDefinitionNode -} - -function mapFetchNodeToVariableDefinitions( - variableUsages: VariableUsages, -): VariableDefinitionNode[] { - return variableUsages ? Object.values(variableUsages) : []; -} - -function operationForRootFetch({ - selectionSet, - variableUsages, - internalFragments, - operation = 'query', -}: { - selectionSet: SelectionSetNode; - variableUsages: VariableUsages; - internalFragments: Set; - operation?: OperationTypeNode; -}): DocumentNode { - return { - kind: Kind.DOCUMENT, - definitions: [ - { - kind: Kind.OPERATION_DEFINITION, - operation, - selectionSet, - variableDefinitions: mapFetchNodeToVariableDefinitions(variableUsages), - }, - ...internalFragments, - ], - }; -} - -function operationForEntitiesFetch({ - selectionSet, - variableUsages, - internalFragments, -}: { - selectionSet: SelectionSetNode; - variableUsages: VariableUsages; - internalFragments: Set; -}): DocumentNode { - const representationsVariable = { - kind: Kind.VARIABLE, - name: { kind: Kind.NAME, value: 'representations' }, - }; - - return { - kind: Kind.DOCUMENT, - definitions: [ - { - kind: Kind.OPERATION_DEFINITION, - operation: 'query', - variableDefinitions: ([ - { - kind: Kind.VARIABLE_DEFINITION, - variable: representationsVariable, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { kind: Kind.NAME, value: '_Any' }, - }, - }, - }, - }, - }, - ] as VariableDefinitionNode[]).concat( - mapFetchNodeToVariableDefinitions(variableUsages), - ), - selectionSet: { - kind: Kind.SELECTION_SET, - selections: [ - { - kind: Kind.FIELD, - name: { kind: Kind.NAME, value: '_entities' }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: representationsVariable.name.value, - }, - value: representationsVariable, - }, - ], - selectionSet, - }, - ], - }, - }, - ...internalFragments, - ], - }; -} - -// Wraps the given nodes in a ParallelNode or SequenceNode, unless there's only -// one node, in which case it is returned directly. Any nodes of the same kind -// in the given list have their sub-nodes flattened into the list: ie, -// flatWrap('Sequence', [a, flatWrap('Sequence', b, c), d]) returns a SequenceNode -// with four children. -function flatWrap( - kind: ParallelNode['kind'] | SequenceNode['kind'], - nodes: PlanNode[], -): PlanNode { - if (nodes.length === 0) { - throw Error('programming error: should always be called with nodes'); - } - if (nodes.length === 1) { - return nodes[0]; - } - return { - kind, - nodes: nodes.flatMap(n => (n.kind === kind ? n.nodes : [n])), - } as PlanNode; -} - -function splitRootFields( - context: QueryPlanningContext, - fields: FieldSet, -): FetchGroup[] { - const groupsByService: { - [serviceName: string]: FetchGroup; - } = Object.create(null); - - function groupForService(serviceName: string) { - let group = groupsByService[serviceName]; - - if (!group) { - group = new FetchGroup(serviceName); - groupsByService[serviceName] = group; - } - - return group; - } - - splitFields(context, [], fields, field => { - const { scope, fieldNode, fieldDef } = field; - const { parentType } = scope; - - const owningService = context.getOwningService(parentType, fieldDef); - - if (!owningService) { - throw new GraphQLError( - `Couldn't find owning service for field "${parentType.name}.${fieldDef.name}"`, - fieldNode, - ); - } - - return groupForService(owningService); - }); - - return Object.values(groupsByService); -} - -// For mutations, we need to respect the order of the fields, in order to -// determine which fields can be batched together in the same request. If -// they're "split" by fields belonging to other services, then we need to manage -// the proper sequencing at the gateway level. In this example, we need 3 -// FetchGroups (requests) in sequence: -// -// mutation abc { -// createReview() # reviews service (1) -// updateReview() # reviews service (1) -// login() # account service (2) -// deleteReview() # reviews service (3) -// } -function splitRootFieldsSerially( - context: QueryPlanningContext, - fields: FieldSet, -): FetchGroup[] { - const fetchGroups: FetchGroup[] = []; - - function groupForField(serviceName: string) { - let group: FetchGroup; - - // If the most recent FetchGroup in the array belongs to the same service, - // the field in question can be batched within that group. - const previousGroup = fetchGroups[fetchGroups.length - 1]; - if (previousGroup && previousGroup.serviceName === serviceName) { - return previousGroup; - } - - // If there's no previous group, or the previous group is from a different - // service, then we need to add a new FetchGroup. - group = new FetchGroup(serviceName); - fetchGroups.push(group); - - return group; - } - - splitFields(context, [], fields, field => { - const { scope, fieldNode, fieldDef } = field; - const { parentType } = scope; - - const owningService = context.getOwningService(parentType, fieldDef); - - if (!owningService) { - throw new GraphQLError( - `Couldn't find owning service for field "${parentType.name}.${fieldDef.name}"`, - fieldNode, - ); - } - - return groupForField(owningService); - }); - - return fetchGroups; -} - -function splitSubfields( - context: QueryPlanningContext, - path: ResponsePath, - fields: FieldSet, - parentGroup: FetchGroup, -) { - splitFields(context, path, fields, field => { - const { scope, fieldNode, fieldDef } = field; - const { parentType } = scope; - - let baseService, owningService; - - const parentTypeFederationMetadata = getFederationMetadata(parentType); - if (parentTypeFederationMetadata?.isValueType) { - baseService = parentGroup.serviceName; - owningService = parentGroup.serviceName; - } else { - baseService = context.getBaseService(parentType); - owningService = context.getOwningService(parentType, fieldDef); - } - - if (!baseService) { - throw new GraphQLError( - `Couldn't find base service for type "${parentType.name}"`, - fieldNode, - ); - } - - if (!owningService) { - throw new GraphQLError( - `Couldn't find owning service for field "${parentType.name}.${fieldDef.name}"`, - fieldNode, - ); - } - // Is the field defined on the base service? - if (owningService === baseService) { - // Can we fetch the field from the parent group? - if ( - owningService === parentGroup.serviceName || - parentGroup.providedFields.some(matchesField(field)) - ) { - return parentGroup; - } else { - // We need to fetch the key fields from the parent group first, and then - // use a dependent fetch from the owning service. - let keyFields = context.getKeyFields({ - parentType, - serviceName: parentGroup.serviceName, - }); - if ( - keyFields.length === 0 || - (keyFields.length === 1 && - keyFields[0].fieldDef.name === '__typename') - ) { - // Only __typename key found. - // In some cases, the parent group does not have any @key directives. - // Fall back to owning group's keys - keyFields = context.getKeyFields({ - parentType, - serviceName: owningService, - }); - } - return parentGroup.dependentGroupForService(owningService, keyFields); - } - } else { - // It's an extension field, so we need to fetch the required fields first. - const requiredFields = context.getRequiredFields( - parentType, - fieldDef, - owningService, - ); - - // Can we fetch the required fields from the parent group? - if ( - requiredFields.every(requiredField => - parentGroup.providedFields.some(matchesField(requiredField)), - ) - ) { - if (owningService === parentGroup.serviceName) { - return parentGroup; - } else { - return parentGroup.dependentGroupForService( - owningService, - requiredFields, - ); - } - } else { - // We need to go through the base group first. - - const keyFields = context.getKeyFields({ - parentType, - serviceName: parentGroup.serviceName, - }); - - if (!keyFields) { - throw new GraphQLError( - `Couldn't find keys for type "${parentType.name}}" in service "${baseService}"`, - fieldNode, - ); - } - - if (baseService === parentGroup.serviceName) { - return parentGroup.dependentGroupForService( - owningService, - requiredFields, - ); - } - - const baseGroup = parentGroup.dependentGroupForService( - baseService, - keyFields, - ); - - return baseGroup.dependentGroupForService( - owningService, - requiredFields, - ); - } - } - }); -} - -function splitFields( - context: QueryPlanningContext, - path: ResponsePath, - fields: FieldSet, - groupForField: (field: Field) => FetchGroup, -) { - for (const fieldsForResponseName of groupByResponseName(fields).values()) { - for (const [parentType, fieldsForParentType] of groupByParentType(fieldsForResponseName)) { - // Field nodes that share the same response name and parent type are guaranteed - // to have the same field name and arguments. We only need the other nodes when - // merging selection sets, to take node-specific subfields and directives - // into account. - - const field = fieldsForParentType[0]; - const { scope, fieldDef } = field; - - // We skip `__typename` for root types. - if (fieldDef.name === TypeNameMetaFieldDef.name) { - const { schema } = context; - const roots = [ - schema.getQueryType(), - schema.getMutationType(), - schema.getSubscriptionType(), - ] - .filter(isNotNullOrUndefined) - .map(type => type.name); - if (roots.indexOf(parentType.name) > -1) continue; - } - - // We skip introspection fields like `__schema` and `__type`. - if (isIntrospectionType(getNamedType(fieldDef.type))) { - continue; - } - - if (isObjectType(parentType) && scope.possibleTypes.includes(parentType)) { - // If parent type is an object type, we can directly look for the right - // group. - const group = groupForField(field as Field); - group.fields.push( - completeField( - context, - scope as Scope, - group, - path, - fieldsForParentType, - ), - ); - } else { - // For interfaces however, we need to look at all possible runtime types. - - /** - * The following is an optimization to prevent an explosion of type - * conditions to services when it isn't needed. If all possible runtime - * types can be fufilled by only one service then we don't need to - * expand the fields into unique type conditions. - */ - - // Collect all of the field defs on the possible runtime types - const possibleFieldDefs = scope.possibleTypes.map( - runtimeType => context.getFieldDef(runtimeType, field.fieldNode), - ); - - // If none of the field defs have a federation property, this interface's - // implementors can all be resolved within the same service. - const hasNoExtendingFieldDefs = !possibleFieldDefs.some( - getFederationMetadata, - ); - - // With no extending field definitions, we can engage the optimization - if (hasNoExtendingFieldDefs) { - const group = groupForField(field as Field); - group.fields.push( - completeField(context, scope, group, path, fieldsForResponseName) - ); - continue; - } - - // We keep track of which possible runtime parent types can be fetched - // from which group, - const groupsByRuntimeParentTypes = new MultiMap< - FetchGroup, - GraphQLObjectType - >(); - - for (const runtimeParentType of scope.possibleTypes) { - const fieldDef = context.getFieldDef( - runtimeParentType, - field.fieldNode, - ); - groupsByRuntimeParentTypes.add( - groupForField({ - scope: context.newScope(runtimeParentType, scope), - fieldNode: field.fieldNode, - fieldDef, - }), - runtimeParentType, - ); - } - - // We add the field separately for each runtime parent type. - for (const [group, runtimeParentTypes] of groupsByRuntimeParentTypes) { - for (const runtimeParentType of runtimeParentTypes) { - // We need to adjust the fields to contain the right fieldDef for - // their runtime parent type. - - const fieldDef = context.getFieldDef( - runtimeParentType, - field.fieldNode, - ); - - const fieldsWithRuntimeParentType = fieldsForParentType.map(field => ({ - ...field, - fieldDef, - })); - - group.fields.push( - completeField( - context, - context.newScope(runtimeParentType, scope), - group, - path, - fieldsWithRuntimeParentType, - ), - ); - } - } - } - } - } -} - -function completeField( - context: QueryPlanningContext, - scope: Scope, - parentGroup: FetchGroup, - path: ResponsePath, - fields: FieldSet, -): Field { - const { fieldNode, fieldDef } = fields[0]; - const returnType = getNamedType(fieldDef.type); - - if (!isCompositeType(returnType)) { - // FIXME: We should look at all field nodes to make sure we take directives - // into account (or remove directives for the time being). - return { scope, fieldNode, fieldDef }; - } else { - // For composite types, we need to recurse. - - const fieldPath = addPath(path, getResponseName(fieldNode), fieldDef.type); - - const subGroup = new FetchGroup(parentGroup.serviceName); - subGroup.mergeAt = fieldPath; - - subGroup.providedFields = context.getProvidedFields( - fieldDef, - parentGroup.serviceName, - ); - - // For abstract types, we always need to request `__typename` - if (isAbstractType(returnType)) { - subGroup.fields.push({ - scope: context.newScope(returnType, scope), - fieldNode: typenameField, - fieldDef: TypeNameMetaFieldDef, - }); - } - - const subfields = collectSubfields(context, returnType, fields); - splitSubfields(context, fieldPath, subfields, subGroup); - - parentGroup.otherDependentGroups.push(...subGroup.dependentGroups); - - let definition: FragmentDefinitionNode; - let selectionSet = selectionSetFromFieldSet(subGroup.fields, returnType); - - if (context.autoFragmentization && subGroup.fields.length > 2) { - ({ definition, selectionSet } = getInternalFragment( - selectionSet, - returnType, - context, - )); - parentGroup.internalFragments.add(definition); - } - - // "Hoist" internalFragments of the subGroup into the parentGroup so all - // fragments can be included in the final request for the root FetchGroup - subGroup.internalFragments.forEach(fragment => { - parentGroup.internalFragments.add(fragment); - }); - - return { - scope, - fieldNode: { - ...fieldNode, - selectionSet, - }, - fieldDef, - }; - } -} - -function getInternalFragment( - selectionSet: SelectionSetNode, - returnType: GraphQLCompositeType, - context: QueryPlanningContext -) { - const key = JSON.stringify(selectionSet); - if (!context.internalFragments.has(key)) { - const name = `__QueryPlanFragment_${context.internalFragmentCount++}__`; - - const definition: FragmentDefinitionNode = { - kind: Kind.FRAGMENT_DEFINITION, - name: { - kind: Kind.NAME, - value: name, - }, - typeCondition: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: returnType.name, - }, - }, - selectionSet, - }; - - const fragmentSelection: SelectionSetNode = { - kind: Kind.SELECTION_SET, - selections: [ - { - kind: Kind.FRAGMENT_SPREAD, - name: { - kind: Kind.NAME, - value: name, - }, - }, - ], - }; - - context.internalFragments.set(key, { - name, - definition, - selectionSet: fragmentSelection, - }); - } - - return context.internalFragments.get(key)!; -} - -function collectFields( - context: QueryPlanningContext, - scope: Scope, - selectionSet: SelectionSetNode, - fields: FieldSet = [], - visitedFragmentNames: { [fragmentName: string]: boolean } = Object.create( - null, - ), -): FieldSet { - for (const selection of selectionSet.selections) { - switch (selection.kind) { - case Kind.FIELD: - const fieldDef = context.getFieldDef(scope.parentType, selection); - fields.push({ scope, fieldNode: selection, fieldDef }); - break; - case Kind.INLINE_FRAGMENT: { - const newScope = context.newScope(getFragmentCondition(selection), scope); - if (newScope.possibleTypes.length === 0) { - break; - } - - collectFields( - context, - context.newScope(getFragmentCondition(selection), scope), - selection.selectionSet, - fields, - visitedFragmentNames, - ); - break; - } - case Kind.FRAGMENT_SPREAD: - const fragmentName = selection.name.value; - - const fragment = context.fragments[fragmentName]; - if (!fragment) { - continue; - } - - const newScope = context.newScope(getFragmentCondition(fragment), scope); - if (newScope.possibleTypes.length === 0) { - continue; - } - - if (visitedFragmentNames[fragmentName]) { - continue; - } - visitedFragmentNames[fragmentName] = true; - - collectFields( - context, - newScope, - fragment.selectionSet, - fields, - visitedFragmentNames, - ); - break; - } - } - - return fields; - - function getFragmentCondition( - fragment: FragmentDefinitionNode | InlineFragmentNode, - ): GraphQLCompositeType { - const typeConditionNode = fragment.typeCondition; - if (!typeConditionNode) return scope.parentType; - - return typeFromAST( - context.schema, - typeConditionNode, - ) as GraphQLCompositeType; - } -} - -// Collecting subfields collapses parent types, because it merges -// selection sets without taking the runtime parent type of the field -// into account. If we want to keep track of multiple levels of possible -// types, this is where that would need to happen. -export function collectSubfields( - context: QueryPlanningContext, - returnType: GraphQLCompositeType, - fields: FieldSet, -): FieldSet { - let subfields: FieldSet = []; - const visitedFragmentNames = Object.create(null); - - for (const field of fields) { - const selectionSet = field.fieldNode.selectionSet; - - if (selectionSet) { - subfields = collectFields( - context, - context.newScope(returnType), - selectionSet, - subfields, - visitedFragmentNames, - ); - } - } - - return subfields; -} - -class FetchGroup { - constructor( - public readonly serviceName: string, - public readonly fields: FieldSet = [], - public readonly internalFragments: Set = new Set() - ) {} - - requiredFields: FieldSet = []; - providedFields: FieldSet = []; - - mergeAt?: ResponsePath; - - private dependentGroupsByService: { - [serviceName: string]: FetchGroup; - } = Object.create(null); - public otherDependentGroups: FetchGroup[] = []; - - dependentGroupForService(serviceName: string, requiredFields: FieldSet) { - let group = this.dependentGroupsByService[serviceName]; - - if (!group) { - group = new FetchGroup(serviceName); - group.mergeAt = this.mergeAt; - this.dependentGroupsByService[serviceName] = group; - } - - if (requiredFields) { - if (group.requiredFields) { - group.requiredFields.push(...requiredFields); - } else { - group.requiredFields = requiredFields; - } - this.fields.push(...requiredFields); - } - - return group; - } - - get dependentGroups() { - return [ - ...Object.values(this.dependentGroupsByService), - ...this.otherDependentGroups, - ]; - } -} - -// Adapted from buildExecutionContext in graphql-js -export function buildOperationContext( - schema: GraphQLSchema, - document: DocumentNode, - operationName?: string, -): OperationContext { - let operation: OperationDefinitionNode | undefined; - const fragments: { - [fragmentName: string]: FragmentDefinitionNode; - } = Object.create(null); - document.definitions.forEach(definition => { - switch (definition.kind) { - case Kind.OPERATION_DEFINITION: - if (!operationName && operation) { - throw new GraphQLError( - 'Must provide operation name if query contains ' + - 'multiple operations.', - ); - } - if ( - !operationName || - (definition.name && definition.name.value === operationName) - ) { - operation = definition; - } - break; - case Kind.FRAGMENT_DEFINITION: - fragments[definition.name.value] = definition; - break; - } - }); - if (!operation) { - if (operationName) { - throw new GraphQLError(`Unknown operation named "${operationName}".`); - } else { - throw new GraphQLError('Must provide an operation.'); - } - } - - return { schema, operation, fragments }; -} - -export function buildQueryPlanningContext( - { operation, schema, fragments }: OperationContext, - options: BuildQueryPlanOptions, -): QueryPlanningContext { - return new QueryPlanningContext( - schema, - operation, - fragments, - options.autoFragmentization, - ); -} - -export class QueryPlanningContext { - public internalFragments: Map< - string, - { - name: string; - definition: FragmentDefinitionNode; - selectionSet: SelectionSetNode; - } - > = new Map(); - - public internalFragmentCount = 0; - - protected variableDefinitions: { - [name: string]: VariableDefinitionNode; - }; - - constructor( - public readonly schema: GraphQLSchema, - public readonly operation: OperationDefinitionNode, - public readonly fragments: FragmentMap, - public readonly autoFragmentization: boolean, - ) { - this.variableDefinitions = Object.create(null); - visit(operation, { - VariableDefinition: definition => { - this.variableDefinitions[definition.variable.name.value] = definition; - }, - }); - } - - getFieldDef(parentType: GraphQLCompositeType, fieldNode: FieldNode) { - const fieldName = fieldNode.name.value; - - const fieldDef = getFieldDef(this.schema, parentType, fieldName); - - if (!fieldDef) { - throw new GraphQLError( - `Cannot query field "${fieldNode.name.value}" on type "${String( - parentType, - )}"`, - fieldNode, - ); - } - - return fieldDef; - } - - getPossibleTypes( - type: GraphQLAbstractType | GraphQLObjectType, - ): ReadonlyArray { - return isAbstractType(type) ? this.schema.getPossibleTypes(type) : [type]; - } - - getVariableUsages( - selectionSet: SelectionSetNode, - fragments: Set, - ) { - const usages: { - [name: string]: VariableDefinitionNode; - } = Object.create(null); - - // Construct a document of the selection set and fragment definitions so we - // can visit them, adding all variable usages to the `usages` object. - const document: DocumentNode = { - kind: Kind.DOCUMENT, - definitions: [ - { kind: Kind.OPERATION_DEFINITION, selectionSet, operation: 'query' }, - ...Array.from(fragments), - ], - }; - - visit(document, { - Variable: (node) => { - usages[node.name.value] = this.variableDefinitions[node.name.value]; - }, - }); - - return usages; - } - - newScope( - parentType: TParent, - enclosingScope?: Scope, - ): Scope { - return { - parentType, - possibleTypes: enclosingScope - ? this.getPossibleTypes(parentType).filter(type => - enclosingScope.possibleTypes.includes(type), - ) - : this.getPossibleTypes(parentType), - enclosingScope, - }; - } - - getBaseService(parentType: GraphQLObjectType): string | null { - return (getFederationMetadata(parentType)?.serviceName) || null; - } - - getOwningService( - parentType: GraphQLObjectType, - fieldDef: GraphQLField, - ): string | null { - const fieldFederationMetadata = getFederationMetadata(fieldDef); - if ( - fieldFederationMetadata?.serviceName && - !fieldFederationMetadata?.belongsToValueType - ) { - return fieldFederationMetadata.serviceName; - } else { - return this.getBaseService(parentType); - } - } - - getKeyFields({ - parentType, - serviceName, - fetchAll = false, - }: { - parentType: GraphQLCompositeType; - serviceName: string; - fetchAll?: boolean; - }): FieldSet { - const keyFields: FieldSet = []; - - keyFields.push({ - scope: { - parentType, - possibleTypes: this.getPossibleTypes(parentType), - }, - fieldNode: typenameField, - fieldDef: TypeNameMetaFieldDef, - }); - - for (const possibleType of this.getPossibleTypes(parentType)) { - const keys = getFederationMetadata(possibleType)?.keys?.[serviceName]; - - if (!(keys && keys.length > 0)) continue; - - if (fetchAll) { - keyFields.push( - ...keys.flatMap(key => - collectFields(this, this.newScope(possibleType), { - kind: Kind.SELECTION_SET, - selections: key, - }), - ), - ); - } else { - keyFields.push( - ...collectFields(this, this.newScope(possibleType), { - kind: Kind.SELECTION_SET, - selections: keys[0], - }), - ); - } - } - - return keyFields; - } - - getRequiredFields( - parentType: GraphQLCompositeType, - fieldDef: GraphQLField, - serviceName: string, - ): FieldSet { - const requiredFields: FieldSet = []; - - requiredFields.push(...this.getKeyFields({ parentType, serviceName })); - - const fieldFederationMetadata = getFederationMetadata(fieldDef); - if (fieldFederationMetadata?.requires) { - requiredFields.push( - ...collectFields(this, this.newScope(parentType), { - kind: Kind.SELECTION_SET, - selections: fieldFederationMetadata.requires, - }), - ); - } - - return requiredFields; - } - - getProvidedFields( - fieldDef: GraphQLField, - serviceName: string, - ): FieldSet { - const returnType = getNamedType(fieldDef.type); - if (!isCompositeType(returnType)) return []; - - const providedFields: FieldSet = []; - - providedFields.push( - ...this.getKeyFields({ - parentType: returnType, - serviceName, - fetchAll: true, - }), - ); - - const fieldFederationMetadata = getFederationMetadata(fieldDef); - if (fieldFederationMetadata?.provides) { - providedFields.push( - ...collectFields(this, this.newScope(returnType), { - kind: Kind.SELECTION_SET, - selections: fieldFederationMetadata.provides, - }), - ); - } - - return providedFields; - } -} - -function addPath(path: ResponsePath, responseName: string, type: GraphQLType) { - path = [...path, responseName]; - - while (!isNamedType(type)) { - if (isListType(type)) { - path.push('@'); - } - - type = type.ofType; - } - - return path; -} diff --git a/packages/apollo-gateway/src/cache.ts b/packages/apollo-gateway/src/cache.ts deleted file mode 100644 index 5393d9e5e70..00000000000 --- a/packages/apollo-gateway/src/cache.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { CacheManager } from 'make-fetch-happen'; -import { Request, Response, Headers } from 'apollo-server-env'; -import { InMemoryLRUCache } from 'apollo-server-caching'; - -const MAX_SIZE = 5 * 1024 * 1024; // 5MB - -function cacheKey(request: Request) { - return `gateway:request-cache:${request.method}:${request.url}`; -} - -interface CachedRequest { - body: string; - status: number; - statusText: string; - headers: Headers; -} - -export class HttpRequestCache implements CacheManager { - constructor( - public cache: InMemoryLRUCache = new InMemoryLRUCache({ - maxSize: MAX_SIZE, - }), - ) {} - - // Return true if entry exists, else false - async delete(request: Request) { - const key = cacheKey(request); - const entry = await this.cache.get(key); - await this.cache.delete(key); - return Boolean(entry); - } - - async put(request: Request, response: Response) { - // A `HEAD` request has no body to cache and a 304 response could have - // only been negotiated by using a cached body that was still valid. - // Therefore, we do NOT write to the cache in either of these cases. - // Without avoiding this, we will invalidate the cache, thus causing - // subsequent conditional requests (e.g., `If-None-Match: "MD%") to be - // lacking content to conditionally request against and necessitating - // a full request/response. - if (request.method === "HEAD" || response.status === 304) { - return response; - } - - const body = await response.text(); - - this.cache.set(cacheKey(request), { - body, - status: response.status, - statusText: response.statusText, - headers: response.headers, - }); - - return new Response(body, response); - } - - async match(request: Request) { - return this.cache.get(cacheKey(request)).then(response => { - if (response) { - const { body, ...requestInit } = response; - return new Response(body, requestInit); - } - return; - }); - } -} diff --git a/packages/apollo-gateway/src/datasources/LocalGraphQLDataSource.ts b/packages/apollo-gateway/src/datasources/LocalGraphQLDataSource.ts deleted file mode 100644 index 979e3149791..00000000000 --- a/packages/apollo-gateway/src/datasources/LocalGraphQLDataSource.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { GraphQLRequestContext, GraphQLResponse } from 'apollo-server-types'; -import { - GraphQLSchema, - graphql, - graphqlSync, - DocumentNode, - parse, -} from 'graphql'; -import { enableGraphQLExtensions } from 'graphql-extensions'; -import { GraphQLDataSource } from './types'; - -export class LocalGraphQLDataSource = Record> implements GraphQLDataSource { - constructor(public readonly schema: GraphQLSchema) { - // FIXME: This is needed to enable support for `resolveObject`, but we - // should move that to `apollo-graphql` - enableGraphQLExtensions(schema); - } - - async process({ - request, - context, - }: Pick, 'request' | 'context'>): Promise< - GraphQLResponse - > { - return graphql({ - schema: this.schema, - source: request.query!, - variableValues: request.variables, - operationName: request.operationName, - contextValue: context, - }); - } - - public sdl(): DocumentNode { - const result = graphqlSync({ - schema: this.schema, - source: `{ _service { sdl }}`, - }); - if (result.errors) { - throw new Error(result.errors.map(error => error.message).join('\n\n')); - } - - const sdl = result.data && result.data._service && result.data._service.sdl; - return parse(sdl); - } -} diff --git a/packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts b/packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts deleted file mode 100644 index 1461899f90b..00000000000 --- a/packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { - GraphQLRequestContext, - GraphQLResponse, - ValueOrPromise, - GraphQLRequest, -} from 'apollo-server-types'; -import { - ApolloError, - AuthenticationError, - ForbiddenError, -} from 'apollo-server-errors'; -import { - fetch, - Request, - Headers, - Response, -} from 'apollo-server-env'; -import { isObject } from '../utilities/predicates'; -import { GraphQLDataSource } from './types'; -import createSHA from 'apollo-server-core/dist/utils/createSHA'; - -export class RemoteGraphQLDataSource = Record> implements GraphQLDataSource { - fetcher: typeof fetch = fetch; - - constructor( - config?: Partial> & - object & - ThisType>, - ) { - if (config) { - return Object.assign(this, config); - } - } - - url!: string; - - /** - * Whether the downstream request should be made with automated persisted - * query (APQ) behavior enabled. - * - * @remarks When enabled, the request to the downstream service will first be - * attempted using a SHA-256 hash of the operation rather than including the - * operation itself. If the downstream server supports APQ and has this - * operation registered in its APQ storage, it will be able to complete the - * request without the entirety of the operation document being transmitted. - * - * In the event that the downstream service is unaware of the operation, it - * will respond with an `PersistedQueryNotFound` error and it will be resent - * with the full operation body for fulfillment. - * - * Generally speaking, when the downstream server is processing similar - * operations repeatedly, APQ can offer substantial network savings in terms - * of bytes transmitted over the wire between gateways and downstream servers. - */ - apq: boolean = false; - - async process({ - request, - context, - }: Pick, 'request' | 'context'>): Promise< - GraphQLResponse - > { - // Respect incoming http headers (eg, apollo-federation-include-trace). - const headers = (request.http && request.http.headers) || new Headers(); - headers.set('Content-Type', 'application/json'); - - request.http = { - method: 'POST', - url: this.url, - headers, - }; - - if (this.willSendRequest) { - await this.willSendRequest({ request, context }); - } - - if (!request.query) { - throw new Error("Missing query"); - } - - const apqHash = createSHA('sha256') - .update(request.query) - .digest('hex'); - - const { query, ...requestWithoutQuery } = request; - - const respond = (response: GraphQLResponse, request: GraphQLRequest) => - typeof this.didReceiveResponse === "function" - ? this.didReceiveResponse({ response, request, context }) - : response; - - if (this.apq) { - // Take the original extensions and extend them with - // the necessary "extensions" for APQ handshaking. - requestWithoutQuery.extensions = { - ...request.extensions, - persistedQuery: { - version: 1, - sha256Hash: apqHash, - }, - }; - - const apqOptimisticResponse = - await this.sendRequest(requestWithoutQuery, context); - - // If we didn't receive notice to retry with APQ, then let's - // assume this is the best result we'll get and return it! - if ( - !apqOptimisticResponse.errors || - !apqOptimisticResponse.errors.find(error => - error.message === 'PersistedQueryNotFound') - ) { - return respond(apqOptimisticResponse, requestWithoutQuery); - } - } - - // If APQ was enabled, we'll run the same request again, but add in the - // previously omitted `query`. If APQ was NOT enabled, this is the first - // request (non-APQ, all the way). - const requestWithQuery: GraphQLRequest = { - query, - ...requestWithoutQuery, - }; - const response = await this.sendRequest(requestWithQuery, context); - return respond(response, requestWithQuery); - } - - private async sendRequest( - request: GraphQLRequest, - context: TContext, - ): Promise { - - // This would represent an internal programming error since this shouldn't - // be possible in the way that this method is invoked right now. - if (!request.http) { - throw new Error("Internal error: Only 'http' requests are supported.") - } - - // We don't want to serialize the `http` properties into the body that is - // being transmitted. Instead, we want those to be used to indicate what - // we're accessing (e.g. url) and what we access it with (e.g. headers). - const { http, ...requestWithoutHttp } = request; - const fetchRequest = new Request(http.url, { - ...http, - body: JSON.stringify(requestWithoutHttp), - }); - - let fetchResponse: Response | undefined; - - try { - // Use our local `fetcher` to allow for fetch injection - fetchResponse = await this.fetcher(fetchRequest); - - if (!fetchResponse.ok) { - throw await this.errorFromResponse(fetchResponse); - } - - const body = await this.parseBody(fetchResponse, fetchRequest, context); - - if (!isObject(body)) { - throw new Error(`Expected JSON response body, but received: ${body}`); - } - - return { - ...body, - http: fetchResponse, - }; - } catch (error) { - this.didEncounterError(error, fetchRequest, fetchResponse); - throw error; - } - } - - public willSendRequest?( - requestContext: Pick< - GraphQLRequestContext, - 'request' | 'context' - >, - ): ValueOrPromise; - - public didReceiveResponse?( - requestContext: Required, - 'request' | 'response' | 'context'> - >, - ): ValueOrPromise; - - public didEncounterError( - error: Error, - _fetchRequest: Request, - _fetchResponse?: Response - ) { - throw error; - } - - public parseBody( - fetchResponse: Response, - _fetchRequest?: Request, - _context?: TContext, - ): Promise { - const contentType = fetchResponse.headers.get('Content-Type'); - if (contentType && contentType.startsWith('application/json')) { - return fetchResponse.json(); - } else { - return fetchResponse.text(); - } - } - - public async errorFromResponse(response: Response) { - const message = `${response.status}: ${response.statusText}`; - - let error: ApolloError; - if (response.status === 401) { - error = new AuthenticationError(message); - } else if (response.status === 403) { - error = new ForbiddenError(message); - } else { - error = new ApolloError(message); - } - - const body = await this.parseBody(response); - - Object.assign(error.extensions, { - response: { - url: response.url, - status: response.status, - statusText: response.statusText, - body, - }, - }); - - return error; - } -} diff --git a/packages/apollo-gateway/src/datasources/__tests__/LocalGraphQLDataSource.test.ts b/packages/apollo-gateway/src/datasources/__tests__/LocalGraphQLDataSource.test.ts deleted file mode 100644 index 9d710256fdb..00000000000 --- a/packages/apollo-gateway/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { LocalGraphQLDataSource } from '../LocalGraphQLDataSource'; -import { buildFederatedSchema } from '@apollo/federation'; -import gql from 'graphql-tag'; - -describe('constructing requests', () => { - it('accepts context', async () => { - const typeDefs = gql` - type Query { - me: User - } - type User { - id: ID - name: String! - } - `; - const resolvers = { - Query: { - me(_, __, { userId }) { - const users = [ - { id: 1, name: 'otherGuy' }, - { id: 2, name: 'james' }, - { - id: 3, - name: 'someoneElse', - }, - ]; - return users.find(user => user.id === userId); - }, - }, - }; - const schema = buildFederatedSchema([{ typeDefs, resolvers }]); - - const DataSource = new LocalGraphQLDataSource(schema); - - const { data } = await DataSource.process({ - request: { - query: '{ me { name } }', - }, - context: { userId: 2 }, - }); - - expect(data).toEqual({ me: { name: 'james' } }); - }); -}); diff --git a/packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts b/packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts deleted file mode 100644 index f81b39afb18..00000000000 --- a/packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { fetch } from '__mocks__/apollo-server-env'; - -import { - ApolloError, - AuthenticationError, - ForbiddenError, -} from 'apollo-server-errors'; - -import { RemoteGraphQLDataSource } from '../RemoteGraphQLDataSource'; -import { Headers } from 'apollo-server-env'; -import { GraphQLRequestContext } from 'apollo-server-types'; -import { Response } from '../../../../../../apollo-tooling/packages/apollo-env/lib'; - -beforeEach(() => { - fetch.mockReset(); -}); - -describe('constructing requests', () => { - describe('without APQ', () => { - it('stringifies a request with a query', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - apq: false, - }); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { query: '{ me { name } }' }, - context: {}, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toBeCalledTimes(1); - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/foo', - body: { query: '{ me { name } }' }, - }); - }); - - it('passes variables', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - apq: false, - }); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { - query: '{ me { name } }', - variables: { id: '1' }, - }, - context: {}, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toBeCalledTimes(1); - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/foo', - body: { query: '{ me { name } }', variables: { id: '1' } }, - }); - }); - }); - - describe('with APQ', () => { - // When changing this, adjust the SHA-256 hash below as well. - const query = '{ me { name } }'; - - // This is a SHA-256 hash of `query` above. - const sha256Hash = - "b8d9506e34c83b0e53c2aa463624fcea354713bc38f95276e6f0bd893ffb5b88"; - - describe('miss', () => { - const apqNotFoundResponse = { - "errors": [ - { - "message": "PersistedQueryNotFound", - "extensions": { - "code": "PERSISTED_QUERY_NOT_FOUND", - "exception": { - "stacktrace": ["PersistedQueryNotFoundError: PersistedQueryNotFound"] - } - } - } - ] - }; - - it('stringifies a request with a query', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - apq: true, - }); - - fetch.mockJSONResponseOnce(apqNotFoundResponse); - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { query }, - context: {}, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toBeCalledTimes(2); - expect(fetch).toHaveFetchedNth(1, { - url: 'https://api.example.com/foo', - body: { - extensions: { - persistedQuery: { - version: 1, - sha256Hash, - } - } - }, - }); - expect(fetch).toHaveFetchedNth(2, { - url: 'https://api.example.com/foo', - body: { - query, - extensions: { - persistedQuery: { - version: 1, - sha256Hash, - } - } - }, - }); - }); - - it('passes variables', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - apq: true, - }); - - fetch.mockJSONResponseOnce(apqNotFoundResponse); - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { - query, - variables: { id: '1' }, - }, - context: {}, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toBeCalledTimes(2); - expect(fetch).toHaveFetchedNth(1, { - url: 'https://api.example.com/foo', - body: { - variables: { id: '1' }, - extensions: { - persistedQuery: { - version: 1, - sha256Hash, - } - } - }, - }); - - expect(fetch).toHaveFetchedNth(2, { - url: 'https://api.example.com/foo', - body: { - query, - variables: { id: '1' }, - extensions: { - persistedQuery: { - version: 1, - sha256Hash, - } - } - }, - }); - }); - }); - - describe('hit', () => { - it('stringifies a request with a query', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - apq: true, - }); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { query }, - context: {}, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toBeCalledTimes(1); - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/foo', - body: { - extensions: { - persistedQuery: { - version: 1, - sha256Hash, - } - } - }, - }); - }); - - it('passes variables', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - apq: true, - }); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { - query, - variables: { id: '1' }, - }, - context: {}, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toBeCalledTimes(1); - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/foo', - body: { - variables: { id: '1' }, - extensions: { - persistedQuery: { - version: 1, - sha256Hash, - } - } - }, - }); - }); - }); - }); -}); - -describe('fetcher', () => { - it('uses a custom provided `fetcher`', async () => { - const injectedFetch = fetch.mockJSONResponseOnce({ data: { injected: true } }); - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - fetcher: injectedFetch, - }); - - const { data } = await DataSource.process({ - request: { - query: '{ me { name } }', - variables: { id: '1' }, - }, - context: {}, - }); - - expect(injectedFetch).toHaveBeenCalled(); - expect(data).toEqual({injected: true}); - - }); - -}); - -describe('willSendRequest', () => { - it('allows for modifying variables', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - willSendRequest: ({ request }) => { - request.variables = JSON.stringify(request.variables); - }, - }); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { - query: '{ me { name } }', - variables: { id: '1' }, - }, - context: {}, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/foo', - body: { - query: '{ me { name } }', - variables: JSON.stringify({ id: '1' }), - }, - }); - }); - - it('accepts context', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - willSendRequest: ({ request, context }) => { - request.http.headers.set('x-user-id', context.userId); - }, - }); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const { data } = await DataSource.process({ - request: { - query: '{ me { name } }', - variables: { id: '1' }, - }, - context: { userId: '1234' }, - }); - - expect(data).toEqual({ me: 'james' }); - expect(fetch).toHaveFetched({ - url: 'https://api.example.com/foo', - body: { - query: '{ me { name } }', - variables: { id: '1' }, - }, - headers: { - 'x-user-id': '1234', - }, - }); - }); -}); - -describe('didReceiveResponse', () => { - it('can accept and modify context', async () => { - interface MyContext { - surrogateKeys: string[]; - } - - class MyDataSource extends RemoteGraphQLDataSource { - url = 'https://api.example.com/foo'; - - didReceiveResponse({ - request, - response, - }: Required, - 'request' | 'response' | 'context' - >>) { - const surrogateKeys = - request.http && request.http.headers.get('surrogate-keys'); - if (surrogateKeys) { - context.surrogateKeys.push(...surrogateKeys.split(' ')); - } - return response; - } - } - - const DataSource = new MyDataSource(); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - const context: MyContext = { surrogateKeys: [] }; - await DataSource.process({ - request: { - query: '{ me { name } }', - variables: { id: '1' }, - http: { - method: 'GET', - url: 'https://api.example.com/foo', - headers: new Headers({ 'Surrogate-Keys': 'abc def' }), - }, - }, - context, - }); - - expect(context).toEqual({ surrogateKeys: ['abc', 'def'] }); - }); - - it('is only called once', async () => { - class MyDataSource extends RemoteGraphQLDataSource { - url = 'https://api.example.com/foo'; - - didReceiveResponse({ - response, - }: Required, - 'request' | 'response' | 'context' - >>) { - return response; - } - } - - const DataSource = new MyDataSource(); - const spyDidReceiveResponse = - jest.spyOn(DataSource, 'didReceiveResponse'); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - await DataSource.process({ - request: { - query: '{ me { name } }', - variables: { id: '1' }, - }, - context: {}, - }); - - expect(spyDidReceiveResponse).toHaveBeenCalledTimes(1); - - }); - - // APQ makes two requests, so make sure only one calls the response hook. - it('is only called once when apq is enabled', async () => { - class MyDataSource extends RemoteGraphQLDataSource { - url = 'https://api.example.com/foo'; - apq = true; - - didReceiveResponse({ - response, - }: Required, - 'request' | 'response' | 'context' - >>) { - return response; - } - } - - const DataSource = new MyDataSource(); - const spyDidReceiveResponse = jest.spyOn(DataSource, 'didReceiveResponse'); - - fetch.mockJSONResponseOnce({ data: { me: 'james' } }); - - await DataSource.process({ - request: { - query: '{ me { name } }', - variables: { id: '1' }, - }, - context: {}, - }); - - expect(spyDidReceiveResponse).toHaveBeenCalledTimes(1); - - }); -}); - -describe('error handling', () => { - it('throws an AuthenticationError when the response status is 401', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - }); - - fetch.mockResponseOnce('Invalid token', undefined, 401); - - const result = DataSource.process({ - request: { query: '{ me { name } }' }, - context: {}, - }); - await expect(result).rejects.toThrow(AuthenticationError); - await expect(result).rejects.toMatchObject({ - extensions: { - code: 'UNAUTHENTICATED', - response: { - status: 401, - body: 'Invalid token', - }, - }, - }); - }); - - it('throws a ForbiddenError when the response status is 403', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - }); - - fetch.mockResponseOnce('No access', undefined, 403); - - const result = DataSource.process({ - request: { query: '{ me { name } }' }, - context: {}, - }); - await expect(result).rejects.toThrow(ForbiddenError); - await expect(result).rejects.toMatchObject({ - extensions: { - code: 'FORBIDDEN', - response: { - status: 403, - body: 'No access', - }, - }, - }); - }); - - it('throws an ApolloError when the response status is 500', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - }); - - fetch.mockResponseOnce('Oops', undefined, 500); - - const result = DataSource.process({ - request: { query: '{ me { name } }' }, - context: {}, - }); - await expect(result).rejects.toThrow(ApolloError); - await expect(result).rejects.toMatchObject({ - extensions: { - response: { - status: 500, - body: 'Oops', - }, - }, - }); - }); - - it('puts JSON error responses on the error as an object', async () => { - const DataSource = new RemoteGraphQLDataSource({ - url: 'https://api.example.com/foo', - }); - - fetch.mockResponseOnce( - JSON.stringify({ - errors: [ - { - message: 'Houston, we have a problem.', - }, - ], - }), - { 'Content-Type': 'application/json' }, - 500, - ); - - const result = DataSource.process({ - request: { query: '{ me { name } }' }, - context: {}, - }); - await expect(result).rejects.toThrow(ApolloError); - await expect(result).rejects.toMatchObject({ - extensions: { - response: { - status: 500, - body: { - errors: [ - { - message: 'Houston, we have a problem.', - }, - ], - }, - }, - }, - }); - }); -}); diff --git a/packages/apollo-gateway/src/datasources/__tests__/tsconfig.json b/packages/apollo-gateway/src/datasources/__tests__/tsconfig.json deleted file mode 100644 index 3e6cf2060ff..00000000000 --- a/packages/apollo-gateway/src/datasources/__tests__/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [ - { "path": "../../../" }, - ] -} diff --git a/packages/apollo-gateway/src/datasources/index.ts b/packages/apollo-gateway/src/datasources/index.ts deleted file mode 100644 index 6ac3ac7ab5b..00000000000 --- a/packages/apollo-gateway/src/datasources/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { LocalGraphQLDataSource } from './LocalGraphQLDataSource'; -export { RemoteGraphQLDataSource } from './RemoteGraphQLDataSource'; -export { GraphQLDataSource } from './types'; diff --git a/packages/apollo-gateway/src/datasources/types.ts b/packages/apollo-gateway/src/datasources/types.ts deleted file mode 100644 index c112c309692..00000000000 --- a/packages/apollo-gateway/src/datasources/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types'; - -export interface GraphQLDataSource = Record> { - process( - request: Pick, 'request' | 'context'>, - ): Promise; -} diff --git a/packages/apollo-gateway/src/executeQueryPlan.ts b/packages/apollo-gateway/src/executeQueryPlan.ts deleted file mode 100644 index 9b919d07ddc..00000000000 --- a/packages/apollo-gateway/src/executeQueryPlan.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { - GraphQLExecutionResult, - GraphQLRequestContext, -} from 'apollo-server-types'; -import { Headers } from 'apollo-server-env'; -import { - execute, - GraphQLError, - Kind, - TypeNameMetaFieldDef, - GraphQLFieldResolver, -} from 'graphql'; -import { Trace, google } from 'apollo-engine-reporting-protobuf'; -import { defaultRootOperationNameLookup } from '@apollo/federation'; -import { GraphQLDataSource } from './datasources/types'; -import { - FetchNode, - PlanNode, - QueryPlan, - ResponsePath, - OperationContext, - QueryPlanSelectionNode, - QueryPlanFieldNode, - getResponseName -} from './QueryPlan'; -import { deepMerge } from './utilities/deepMerge'; - -export type ServiceMap = { - [serviceName: string]: GraphQLDataSource; -}; - -type ResultMap = Record; - -interface ExecutionContext { - queryPlan: QueryPlan; - operationContext: OperationContext; - serviceMap: ServiceMap; - requestContext: GraphQLRequestContext; - errors: GraphQLError[]; -} - -export async function executeQueryPlan( - queryPlan: QueryPlan, - serviceMap: ServiceMap, - requestContext: GraphQLRequestContext, - operationContext: OperationContext, -): Promise { - const errors: GraphQLError[] = []; - - const context: ExecutionContext = { - queryPlan, - operationContext, - serviceMap, - requestContext, - errors, - }; - - let data: ResultMap | undefined | null = Object.create(null); - - const captureTraces = !!( - requestContext.metrics && requestContext.metrics.captureTraces - ); - - if (queryPlan.node) { - const traceNode = await executeNode( - context, - queryPlan.node, - data!, - [], - captureTraces, - ); - if (captureTraces) { - requestContext.metrics!.queryPlanTrace = traceNode; - } - } - - // FIXME: Re-executing the query is a pretty heavy handed way of making sure - // only explicitly requested fields are included and field ordering follows - // the original query. - // It is also used to allow execution of introspection queries though. - try { - ({ data } = await execute({ - schema: operationContext.schema, - document: { - kind: Kind.DOCUMENT, - definitions: [ - operationContext.operation, - ...Object.values(operationContext.fragments), - ], - }, - rootValue: data, - variableValues: requestContext.request.variables, - // We have a special field resolver which ensures we support aliases. - // FIXME: It's _possible_ this will change after `graphql-extensions` is - // deprecated, though not certain. See here, also: https://git.io/Jf8cS. - fieldResolver: defaultFieldResolverWithAliasSupport, - })); - } catch (error) { - return { errors: [error] }; - } - - return errors.length === 0 ? { data } : { errors, data }; -} - -// Note: this function always returns a protobuf QueryPlanNode tree, even if -// we're going to ignore it, because it makes the code much simpler and more -// typesafe. However, it doesn't actually ask for traces from the backend -// service unless we are capturing traces for Engine. -async function executeNode( - context: ExecutionContext, - node: PlanNode, - results: ResultMap | ResultMap[], - path: ResponsePath, - captureTraces: boolean, -): Promise { - if (!results) { - // XXX I don't understand `results` threading well enough to understand when this happens - // and if this corresponds to a real query plan node that should be reported or not. - // - // This may be if running something like `query { fooOrNullFromServiceA { - // somethingFromServiceB } }` and the first field is null, then we don't bother to run the - // inner field at all. - return new Trace.QueryPlanNode(); - } - - switch (node.kind) { - case 'Sequence': { - const traceNode = new Trace.QueryPlanNode.SequenceNode(); - for (const childNode of node.nodes) { - const childTraceNode = await executeNode( - context, - childNode, - results, - path, - captureTraces, - ); - traceNode.nodes.push(childTraceNode!); - } - return new Trace.QueryPlanNode({ sequence: traceNode }); - } - case 'Parallel': { - const childTraceNodes = await Promise.all( - node.nodes.map(async childNode => - executeNode(context, childNode, results, path, captureTraces), - ), - ); - return new Trace.QueryPlanNode({ - parallel: new Trace.QueryPlanNode.ParallelNode({ - nodes: childTraceNodes, - }), - }); - } - case 'Flatten': { - return new Trace.QueryPlanNode({ - flatten: new Trace.QueryPlanNode.FlattenNode({ - responsePath: node.path.map( - id => - new Trace.QueryPlanNode.ResponsePathElement( - typeof id === 'string' ? { fieldName: id } : { index: id }, - ), - ), - node: await executeNode( - context, - node.node, - flattenResultsAtPath(results, node.path), - [...path, ...node.path], - captureTraces, - ), - }), - }); - } - case 'Fetch': { - const traceNode = new Trace.QueryPlanNode.FetchNode({ - serviceName: node.serviceName, - // executeFetch will fill in the other fields if desired. - }); - try { - await executeFetch( - context, - node, - results, - path, - captureTraces ? traceNode : null, - ); - } catch (error) { - context.errors.push(error); - } - return new Trace.QueryPlanNode({ fetch: traceNode }); - } - } -} - -async function executeFetch( - context: ExecutionContext, - fetch: FetchNode, - results: ResultMap | ResultMap[], - _path: ResponsePath, - traceNode: Trace.QueryPlanNode.FetchNode | null, -): Promise { - const logger = context.requestContext.logger || console; - const service = context.serviceMap[fetch.serviceName]; - if (!service) { - throw new Error(`Couldn't find service with name "${fetch.serviceName}"`); - } - - const entities = Array.isArray(results) ? results : [results]; - if (entities.length < 1) return; - - let variables = Object.create(null); - if (fetch.variableUsages) { - for (const variableName of fetch.variableUsages) { - const providedVariables = context.requestContext.request.variables; - if ( - providedVariables && - typeof providedVariables[variableName] !== 'undefined' - ) { - variables[variableName] = providedVariables[variableName]; - } - } - } - - if (!fetch.requires) { - const dataReceivedFromService = await sendOperation( - context, - fetch.operation, - variables, - ); - - for (const entity of entities) { - deepMerge(entity, dataReceivedFromService); - } - } else { - const requires = fetch.requires; - - const representations: ResultMap[] = []; - const representationToEntity: number[] = []; - - entities.forEach((entity, index) => { - const representation = executeSelectionSet(entity, requires); - if (representation && representation[TypeNameMetaFieldDef.name]) { - representations.push(representation); - representationToEntity.push(index); - } - }); - - if ('representations' in variables) { - throw new Error(`Variables cannot contain key "representations"`); - } - - const dataReceivedFromService = await sendOperation( - context, - fetch.operation, - { ...variables, representations }, - ); - - if (!dataReceivedFromService) { - return; - } - - if ( - !( - dataReceivedFromService._entities && - Array.isArray(dataReceivedFromService._entities) - ) - ) { - throw new Error(`Expected "data._entities" in response to be an array`); - } - - const receivedEntities = dataReceivedFromService._entities; - - if (receivedEntities.length !== representations.length) { - throw new Error( - `Expected "data._entities" to contain ${representations.length} elements`, - ); - } - - for (let i = 0; i < entities.length; i++) { - deepMerge(entities[representationToEntity[i]], receivedEntities[i]); - } - } - - async function sendOperation( - context: ExecutionContext, - source: string, - variables: Record, - ): Promise { - // We declare this as 'any' because it is missing url and method, which - // GraphQLRequest.http is supposed to have if it exists. - let http: any; - - // If we're capturing a trace for Engine, then save the operation text to - // the node we're building and tell the federated service to include a trace - // in its response. - if (traceNode) { - http = { - headers: new Headers({ 'apollo-federation-include-trace': 'ftv1' }), - }; - if ( - context.requestContext.metrics && - context.requestContext.metrics.startHrTime - ) { - traceNode.sentTimeOffset = durationHrTimeToNanos( - process.hrtime(context.requestContext.metrics.startHrTime), - ); - } - traceNode.sentTime = dateToProtoTimestamp(new Date()); - } - - const response = await service.process({ - request: { - query: source, - variables, - http, - }, - context: context.requestContext.context, - }); - - if (response.errors) { - const errors = response.errors.map(error => - downstreamServiceError( - error.message, - fetch.serviceName, - source, - variables, - error.extensions, - error.path, - ), - ); - context.errors.push(...errors); - } - - // If we're capturing a trace for Engine, save the received trace into the - // query plan. - if (traceNode) { - traceNode.receivedTime = dateToProtoTimestamp(new Date()); - - if (response.extensions && response.extensions.ftv1) { - const traceBase64 = response.extensions.ftv1; - - let traceBuffer: Buffer | undefined; - let traceParsingFailed = false; - try { - // XXX support non-Node implementations by using Uint8Array? protobufjs - // supports that, but there's not a no-deps base64 implementation. - traceBuffer = Buffer.from(traceBase64, 'base64'); - } catch (err) { - logger.error( - `error decoding base64 for federated trace from ${fetch.serviceName}: ${err}`, - ); - traceParsingFailed = true; - } - - if (traceBuffer) { - try { - const trace = Trace.decode(traceBuffer); - traceNode.trace = trace; - } catch (err) { - logger.error( - `error decoding protobuf for federated trace from ${fetch.serviceName}: ${err}`, - ); - traceParsingFailed = true; - } - } - if (traceNode.trace) { - // Federation requires the root operations in the composed schema - // to have the default names (Query, Mutation, Subscription) even - // if the implementing services choose different names, so we override - // whatever the implementing service reported here. - const rootTypeName = - defaultRootOperationNameLookup[ - context.operationContext.operation.operation - ]; - traceNode.trace.root?.child?.forEach((child) => { - child.parentType = rootTypeName; - }); - } - traceNode.traceParsingFailed = traceParsingFailed; - } - } - - return response.data; - } -} - -/** - * - * @param source Result of GraphQL execution. - * @param selectionSet - */ -function executeSelectionSet( - source: Record | null, - selections: QueryPlanSelectionNode[], -): Record | null { - - // If the underlying service has returned null for the parent (source) - // then there is no need to iterate through the parent's selection set - if (source === null) { - return null; - } - - const result: Record = Object.create(null); - - for (const selection of selections) { - switch (selection.kind) { - case Kind.FIELD: - const responseName = getResponseName(selection as QueryPlanFieldNode); - const selections = (selection as QueryPlanFieldNode).selections; - - if (typeof source[responseName] === 'undefined') { - throw new Error(`Field "${responseName}" was not found in response.`); - } - if (Array.isArray(source[responseName])) { - result[responseName] = source[responseName].map((value: any) => - selections ? executeSelectionSet(value, selections) : value, - ); - } else if (selections) { - result[responseName] = executeSelectionSet( - source[responseName], - selections, - ); - } else { - result[responseName] = source[responseName]; - } - break; - case Kind.INLINE_FRAGMENT: - if (!selection.typeCondition) continue; - - const typename = source && source['__typename']; - if (!typename) continue; - - if (typename === selection.typeCondition) { - deepMerge( - result, - executeSelectionSet(source, selection.selections), - ); - } - break; - } - } - - return result; -} - -function flattenResultsAtPath(value: any, path: ResponsePath): any { - if (path.length === 0) return value; - if (value === undefined || value === null) return value; - - const [current, ...rest] = path; - if (current === '@') { - return value.flatMap((element: any) => flattenResultsAtPath(element, rest)); - } else { - return flattenResultsAtPath(value[current], rest); - } -} - -function downstreamServiceError( - message: string | undefined, - serviceName: string, - query: string, - variables?: Record, - extensions?: Record, - path?: ReadonlyArray | undefined, -) { - if (!message) { - message = `Error while fetching subquery from service "${serviceName}"`; - } - extensions = { - code: 'DOWNSTREAM_SERVICE_ERROR', - // XXX The presence of a serviceName in extensions is used to - // determine if this error should be captured for metrics reporting. - serviceName, - query, - variables, - ...extensions, - }; - return new GraphQLError( - message, - undefined, - undefined, - undefined, - path, - undefined, - extensions, - ); -} - -export const defaultFieldResolverWithAliasSupport: GraphQLFieldResolver< - any, - any -> = function(source, args, contextValue, info) { - // ensure source is a value for which property access is acceptable. - if (typeof source === 'object' || typeof source === 'function') { - // if this is an alias, check it first because a downstream service - // would have returned the data *already cast* to an alias responseName - const property = source[info.path.key]; - if (typeof property === 'function') { - return source[info.fieldName](args, contextValue, info); - } - return property; - } -}; - -// Converts an hrtime array (as returned from process.hrtime) to nanoseconds. -// -// ONLY CALL THIS ON VALUES REPRESENTING DELTAS, NOT ON THE RAW RETURN VALUE -// FROM process.hrtime() WITH NO ARGUMENTS. -// -// The entire point of the hrtime data structure is that the JavaScript Number -// type can't represent all int64 values without loss of precision: -// Number.MAX_SAFE_INTEGER nanoseconds is about 104 days. Calling this function -// on a duration that represents a value less than 104 days is fine. Calling -// this function on an absolute time (which is generally roughly time since -// system boot) is not a good idea. -// -// XXX We should probably use google.protobuf.Duration on the wire instead of -// ever trying to store durations in a single number. -function durationHrTimeToNanos(hrtime: [number, number]) { - return hrtime[0] * 1e9 + hrtime[1]; -} - -// Converts a JS Date into a Timestamp. -function dateToProtoTimestamp(date: Date): google.protobuf.Timestamp { - const totalMillis = +date; - const millis = totalMillis % 1000; - return new google.protobuf.Timestamp({ - seconds: (totalMillis - millis) / 1000, - nanos: millis * 1e6, - }); -} diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts deleted file mode 100644 index 278f7102d8c..00000000000 --- a/packages/apollo-gateway/src/index.ts +++ /dev/null @@ -1,818 +0,0 @@ -import { - GraphQLService, - SchemaChangeCallback, - Unsubscriber, - GraphQLServiceEngineConfig, -} from 'apollo-server-core'; -import { - GraphQLExecutionResult, - Logger, - GraphQLRequestContextExecutionDidStart, -} from 'apollo-server-types'; -import { InMemoryLRUCache } from 'apollo-server-caching'; -import { - isObjectType, - isIntrospectionType, - GraphQLSchema, - GraphQLError, - VariableDefinitionNode, -} from 'graphql'; -import { GraphQLSchemaValidationError } from 'apollo-graphql'; -import { composeAndValidate, ServiceDefinition } from '@apollo/federation'; -import loglevel from 'loglevel'; - -import { buildQueryPlan, buildOperationContext } from './buildQueryPlan'; -import { - executeQueryPlan, - ServiceMap, - defaultFieldResolverWithAliasSupport, -} from './executeQueryPlan'; - -import { getServiceDefinitionsFromRemoteEndpoint } from './loadServicesFromRemoteEndpoint'; -import { - getServiceDefinitionsFromStorage, - CompositionMetadata, -} from './loadServicesFromStorage'; - -import { serializeQueryPlan, QueryPlan, OperationContext } from './QueryPlan'; -import { GraphQLDataSource } from './datasources/types'; -import { RemoteGraphQLDataSource } from './datasources/RemoteGraphQLDataSource'; -import { HeadersInit } from 'node-fetch'; -import { getVariableValues } from 'graphql/execution/values'; -import fetcher from 'make-fetch-happen'; -import { HttpRequestCache } from './cache'; -import { fetch } from 'apollo-server-env'; - -export type ServiceEndpointDefinition = Pick; - -interface GatewayConfigBase { - debug?: boolean; - logger?: Logger; - // TODO: expose the query plan in a more flexible JSON format in the future - // and remove this config option in favor of `exposeQueryPlan`. Playground - // should cutover to use the new option when it's built. - __exposeQueryPlanExperimental?: boolean; - buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; - - // experimental observability callbacks - experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - experimental_didFailComposition?: Experimental_DidFailCompositionCallback; - experimental_updateServiceDefinitions?: Experimental_UpdateServiceDefinitions; - experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; - experimental_pollInterval?: number; - experimental_approximateQueryPlanStoreMiB?: number; - experimental_autoFragmentization?: boolean; - fetcher?: typeof fetch; - serviceHealthCheck?: boolean; -} - -interface RemoteGatewayConfig extends GatewayConfigBase { - serviceList: ServiceEndpointDefinition[]; - introspectionHeaders?: HeadersInit; -} - -interface ManagedGatewayConfig extends GatewayConfigBase { - federationVersion?: number; -} -interface LocalGatewayConfig extends GatewayConfigBase { - localServiceList: ServiceDefinition[]; -} - -export type GatewayConfig = - | RemoteGatewayConfig - | LocalGatewayConfig - | ManagedGatewayConfig; - -type DataSourceMap = { - [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; -}; - -function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { - return 'localServiceList' in config; -} - -function isRemoteConfig(config: GatewayConfig): config is RemoteGatewayConfig { - return 'serviceList' in config; -} - -function isManagedConfig( - config: GatewayConfig, -): config is ManagedGatewayConfig { - return !isRemoteConfig(config) && !isLocalConfig(config); -} - -export type Experimental_DidResolveQueryPlanCallback = ({ - queryPlan, - serviceMap, - operationContext, - requestContext, -}: { - readonly queryPlan: QueryPlan; - readonly serviceMap: ServiceMap; - readonly operationContext: OperationContext; - readonly requestContext: GraphQLRequestContextExecutionDidStart>; -}) => void; - -export type Experimental_DidFailCompositionCallback = ({ - errors, - serviceList, - compositionMetadata, -}: { - readonly errors: GraphQLError[]; - readonly serviceList: ServiceDefinition[]; - readonly compositionMetadata?: CompositionMetadata; -}) => void; - -export interface Experimental_CompositionInfo { - serviceDefinitions: ServiceDefinition[]; - schema: GraphQLSchema; - compositionMetadata?: CompositionMetadata; -} - -export type Experimental_DidUpdateCompositionCallback = ( - currentConfig: Experimental_CompositionInfo, - previousConfig?: Experimental_CompositionInfo, -) => void; - -/** - * **Note:** It's possible for a schema to be the same (`isNewSchema: false`) when - * `serviceDefinitions` have changed. For example, during type migration, the - * composed schema may be identical but the `serviceDefinitions` would differ - * since a type has moved from one service to another. - */ -export type Experimental_UpdateServiceDefinitions = ( - config: GatewayConfig, -) => Promise<{ - serviceDefinitions?: ServiceDefinition[]; - compositionMetadata?: CompositionMetadata; - isNewSchema: boolean; -}>; - -type Await = T extends Promise ? U : T; - -// Local state to track whether particular UX-improving warning messages have -// already been emitted. This is particularly useful to prevent recurring -// warnings of the same type in, e.g. repeating timers, which don't provide -// additional value when they are repeated over and over during the life-time -// of a server. -type WarnedStates = { - remoteWithLocalConfig?: boolean; -}; - -export const GCS_RETRY_COUNT = 5; - -export function getDefaultGcsFetcher() { - return fetcher.defaults({ - cacheManager: new HttpRequestCache(), - // All headers should be lower-cased here, as `make-fetch-happen` - // treats differently cased headers as unique (unlike the `Headers` object). - // @see: https://git.io/JvRUa - headers: { - 'user-agent': `apollo-gateway/${require('../package.json').version}`, - }, - retry: { - retries: GCS_RETRY_COUNT, - // The default factor: expected attempts at 0, 1, 3, 7, 15, and 31 seconds elapsed - factor: 2, - // 1 second - minTimeout: 1000, - randomize: true, - }, - }); -} - -export const HEALTH_CHECK_QUERY = - 'query __ApolloServiceHealthCheck__ { __typename }'; -export const SERVICE_DEFINITION_QUERY = - 'query __ApolloGetServiceDefinition__ { _service { sdl } }'; - -export class ApolloGateway implements GraphQLService { - public schema?: GraphQLSchema; - protected serviceMap: DataSourceMap = Object.create(null); - protected config: GatewayConfig; - private logger: Logger; - protected queryPlanStore?: InMemoryLRUCache; - private engineConfig: GraphQLServiceEngineConfig | undefined; - private pollingTimer?: NodeJS.Timer; - private onSchemaChangeListeners = new Set(); - private serviceDefinitions: ServiceDefinition[] = []; - private compositionMetadata?: CompositionMetadata; - private serviceSdlCache = new Map(); - private warnedStates: WarnedStates = Object.create(null); - - private fetcher: typeof fetch = getDefaultGcsFetcher(); - - // Observe query plan, service info, and operation info prior to execution. - // The information made available here will give insight into the resulting - // query plan and the inputs that generated it. - protected experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; - // Observe composition failures and the ServiceList that caused them. This - // enables reporting any issues that occur during composition. Implementors - // will be interested in addressing these immediately. - protected experimental_didFailComposition?: Experimental_DidFailCompositionCallback; - // Used to communicated composition changes, and what definitions caused - // those updates - protected experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; - // Used for overriding the default service list fetcher. This should return - // an array of ServiceDefinition. *This function must be awaited.* - protected updateServiceDefinitions: Experimental_UpdateServiceDefinitions; - // how often service defs should be loaded/updated (in ms) - protected experimental_pollInterval?: number; - - private experimental_approximateQueryPlanStoreMiB?: number; - - constructor(config?: GatewayConfig) { - this.config = { - // TODO: expose the query plan in a more flexible JSON format in the future - // and remove this config option in favor of `exposeQueryPlan`. Playground - // should cutover to use the new option when it's built. - __exposeQueryPlanExperimental: process.env.NODE_ENV !== 'production', - ...config, - }; - - // Setup logging facilities - if (this.config.logger) { - this.logger = this.config.logger; - } else { - // If the user didn't provide their own logger, we'll initialize one. - const loglevelLogger = loglevel.getLogger(`apollo-gateway`); - - // And also support the `debug` option, if it's truthy. - if (this.config.debug === true) { - loglevelLogger.setLevel(loglevelLogger.levels.DEBUG); - } else { - loglevelLogger.setLevel(loglevelLogger.levels.WARN); - } - - this.logger = loglevelLogger; - } - - if (isLocalConfig(this.config)) { - this.schema = this.createSchema(this.config.localServiceList); - } - - this.initializeQueryPlanStore(); - - // this will be overwritten if the config provides experimental_updateServiceDefinitions - this.updateServiceDefinitions = this.loadServiceDefinitions; - - if (config) { - this.updateServiceDefinitions = - config.experimental_updateServiceDefinitions || - this.updateServiceDefinitions; - // set up experimental observability callbacks - this.experimental_didResolveQueryPlan = - config.experimental_didResolveQueryPlan; - this.experimental_didFailComposition = - config.experimental_didFailComposition; - this.experimental_didUpdateComposition = - config.experimental_didUpdateComposition; - - this.experimental_approximateQueryPlanStoreMiB = - config.experimental_approximateQueryPlanStoreMiB; - - if ( - isManagedConfig(config) && - config.experimental_pollInterval && - config.experimental_pollInterval < 10000 - ) { - this.experimental_pollInterval = 10000; - this.logger.warn( - 'Polling Apollo services at a frequency of less than once per 10 seconds (10000) is disallowed. Instead, the minimum allowed pollInterval of 10000 will be used. Please reconfigure your experimental_pollInterval accordingly. If this is problematic for your team, please contact support.', - ); - } else { - this.experimental_pollInterval = config.experimental_pollInterval; - } - - // Warn against using the pollInterval and a serviceList simultaneously - if (config.experimental_pollInterval && isRemoteConfig(config)) { - this.logger.warn( - 'Polling running services is dangerous and not recommended in production. ' + - 'Polling should only be used against a registry. ' + - 'If you are polling running services, use with caution.', - ); - } - - if (config.fetcher) { - this.fetcher = config.fetcher; - } - } - } - - public async load(options?: { engine?: GraphQLServiceEngineConfig }) { - if (options && options.engine) { - if (!options.engine.graphVariant) - this.logger.warn('No graph variant provided. Defaulting to `current`.'); - this.engineConfig = options.engine; - } - - await this.updateComposition(); - if ( - (isManagedConfig(this.config) || this.experimental_pollInterval) && - !this.pollingTimer - ) { - this.pollServices(); - } - - const { graphId, graphVariant } = (options && options.engine) || {}; - const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; - - this.logger.info( - `Gateway successfully loaded schema.\n\t* Mode: ${mode}${ - graphId ? `\n\t* Service: ${graphId}@${graphVariant || 'current'}` : '' - }`, - ); - - return { - // we know this will be here since we're awaiting this.updateComposition - // before here which sets this.schema - schema: this.schema!, - executor: this.executor, - }; - } - - protected async updateComposition(): Promise { - let result: Await>; - this.logger.debug('Checking service definitions...'); - try { - result = await this.updateServiceDefinitions(this.config); - } catch (e) { - this.logger.error( - "Error checking for changes to service definitions: " + - (e && e.message || e) - ); - throw e; - } - - if ( - !result.serviceDefinitions || - JSON.stringify(this.serviceDefinitions) === - JSON.stringify(result.serviceDefinitions) - ) { - this.logger.debug('No change in service definitions since last check.'); - return; - } - - const previousSchema = this.schema; - const previousServiceDefinitions = this.serviceDefinitions; - const previousCompositionMetadata = this.compositionMetadata; - - if (previousSchema) { - this.logger.info("New service definitions were found."); - } - - // Run service health checks before we commit and update the new schema. - // This is the last chance to bail out of a schema update. - if (this.config.serviceHealthCheck) { - // Here we need to construct new datasources based on the new schema info - // so we can check the health of the services we're _updating to_. - const serviceMap = result.serviceDefinitions.reduce( - (serviceMap, serviceDef) => { - serviceMap[serviceDef.name] = { - url: serviceDef.url, - dataSource: this.createDataSource(serviceDef), - }; - return serviceMap; - }, - Object.create(null) as DataSourceMap, - ); - - try { - await this.serviceHealthCheck(serviceMap); - } catch (e) { - this.logger.error( - 'The gateway did not update its schema due to failed service health checks. ' + - 'The gateway will continue to operate with the previous schema and reattempt updates.' + e - ); - throw e; - } - } - - this.compositionMetadata = result.compositionMetadata; - this.serviceDefinitions = result.serviceDefinitions; - - if (this.queryPlanStore) this.queryPlanStore.flush(); - - this.schema = this.createSchema(result.serviceDefinitions); - - // Notify the schema listeners of the updated schema - try { - this.onSchemaChangeListeners.forEach(listener => listener(this.schema!)); - } catch (e) { - this.logger.error( - "An error was thrown from an 'onSchemaChange' listener. " + - "The schema will still update: " + (e && e.message || e)); - } - - if (this.experimental_didUpdateComposition) { - this.experimental_didUpdateComposition( - { - serviceDefinitions: result.serviceDefinitions, - schema: this.schema, - ...(this.compositionMetadata && { - compositionMetadata: this.compositionMetadata, - }), - }, - previousServiceDefinitions && - previousSchema && { - serviceDefinitions: previousServiceDefinitions, - schema: previousSchema, - ...(previousCompositionMetadata && { - compositionMetadata: previousCompositionMetadata, - }), - }, - ); - } - } - - /** - * This can be used without an argument in order to perform an ad-hoc health check - * of the downstream services like so: - * - * @example - * ``` - * try { - * await gateway.serviceHealthCheck(); - * } catch(e) { - * /* your error handling here *\/ - * } - * ``` - * @throws - * @param serviceMap {DataSourceMap} - */ - public serviceHealthCheck(serviceMap: DataSourceMap = this.serviceMap) { - return Promise.all( - Object.entries(serviceMap).map(([name, { dataSource }]) => - dataSource - .process({ request: { query: HEALTH_CHECK_QUERY }, context: {} }) - .then(response => ({ name, response })), - ), - ); - } - - protected createSchema(serviceList: ServiceDefinition[]) { - this.logger.debug( - `Composing schema from service list: \n${serviceList - .map(({ name, url }) => ` ${url || 'local'}: ${name}`) - .join('\n')}`, - ); - - const { schema, errors } = composeAndValidate(serviceList); - - if (errors && errors.length > 0) { - if (this.experimental_didFailComposition) { - this.experimental_didFailComposition({ - errors, - serviceList, - ...(this.compositionMetadata && { - compositionMetadata: this.compositionMetadata, - }), - }); - } - throw new GraphQLSchemaValidationError(errors); - } - - this.createServices(serviceList); - - this.logger.debug('Schema loaded and ready for execution'); - - // FIXME: The comment below may change when `graphql-extensions` is - // removed, as it will be soon. It's not clear if this will be temporary, - // as is suggested, after that time, because we still very much need to - // do this special alias resolving. Original comment: - // this is a temporary workaround for GraphQLFieldExtensions automatic - // wrapping of all fields when using ApolloServer. Here we wrap all fields - // with support for resolving aliases as part of the root value which - // happens because aliases are resolved by sub services and the shape - // of the root value already contains the aliased fields as responseNames - return wrapSchemaWithAliasResolver(schema); - } - - public onSchemaChange(callback: SchemaChangeCallback): Unsubscriber { - this.onSchemaChangeListeners.add(callback); - - return () => { - this.onSchemaChangeListeners.delete(callback); - }; - } - - private async pollServices() { - if (this.pollingTimer) clearTimeout(this.pollingTimer); - - // Sleep for the specified pollInterval before kicking off another round of polling - await new Promise(res => { - this.pollingTimer = setTimeout( - () => res(), - this.experimental_pollInterval || 10000, - ); - // Prevent the Node.js event loop from remaining active (and preventing, - // e.g. process shutdown) by calling `unref` on the `Timeout`. For more - // information, see https://nodejs.org/api/timers.html#timers_timeout_unref. - this.pollingTimer?.unref(); - }); - - try { - await this.updateComposition(); - } catch (err) { - this.logger.error(err && err.message || err); - } - - this.pollServices(); - } - - private createAndCacheDataSource( - serviceDef: ServiceEndpointDefinition, - ): GraphQLDataSource { - // If the DataSource has already been created, early return - if ( - this.serviceMap[serviceDef.name] && - serviceDef.url === this.serviceMap[serviceDef.name].url - ) - return this.serviceMap[serviceDef.name].dataSource; - - const dataSource = this.createDataSource(serviceDef); - - // Cache the created DataSource - this.serviceMap[serviceDef.name] = { url: serviceDef.url, dataSource }; - - return dataSource; - } - - private createDataSource( - serviceDef: ServiceEndpointDefinition, - ): GraphQLDataSource { - if (!serviceDef.url && !isLocalConfig(this.config)) { - this.logger.error( - `Service definition for service ${serviceDef.name} is missing a url`, - ); - } - - return this.config.buildService - ? this.config.buildService(serviceDef) - : new RemoteGraphQLDataSource({ - url: serviceDef.url, - }); - } - - protected createServices(services: ServiceEndpointDefinition[]) { - for (const serviceDef of services) { - this.createAndCacheDataSource(serviceDef); - } - } - - protected async loadServiceDefinitions( - config: GatewayConfig, - ): ReturnType { - // This helper avoids the repetition of options in the two cases this method - // is invoked below. It is a helper, rather than an options object, since it - // depends on the presence of `this.engineConfig`, which is guarded against - // further down in this method in two separate places. - const getManagedConfig = (engineConfig: GraphQLServiceEngineConfig) => { - return getServiceDefinitionsFromStorage({ - graphId: engineConfig.graphId, - apiKeyHash: engineConfig.apiKeyHash, - graphVariant: engineConfig.graphVariant, - federationVersion: - (config as ManagedGatewayConfig).federationVersion || 1, - fetcher: this.fetcher, - }); - }; - - if (isLocalConfig(config) || isRemoteConfig(config)) { - if (this.engineConfig && !this.warnedStates.remoteWithLocalConfig) { - // Only display this warning once per start-up. - this.warnedStates.remoteWithLocalConfig = true; - // This error helps avoid common misconfiguration. - // We don't await this because a local configuration should assume - // remote is unavailable for one reason or another. - getManagedConfig(this.engineConfig).then(() => { - this.logger.warn( - "A local gateway service list is overriding an Apollo Graph " + - "Manager managed configuration. To use the managed " + - "configuration, do not specify a service list locally.", - ); - }).catch(() => {}); // Don't mind errors if managed config is missing. - } - } - - if (isLocalConfig(config)) { - return { isNewSchema: false }; - } - - if (isRemoteConfig(config)) { - const serviceList = config.serviceList.map(serviceDefinition => ({ - ...serviceDefinition, - dataSource: this.createAndCacheDataSource(serviceDefinition), - })); - - return getServiceDefinitionsFromRemoteEndpoint({ - serviceList, - ...(config.introspectionHeaders - ? { headers: config.introspectionHeaders } - : {}), - serviceSdlCache: this.serviceSdlCache, - }); - } - - if (!this.engineConfig) { - throw new Error( - 'When `serviceList` is not set, an Apollo Engine configuration must be provided. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ for more information.', - ); - } - - return getManagedConfig(this.engineConfig); - } - - // XXX Nothing guarantees that the only errors thrown or returned in - // result.errors are GraphQLErrors, even though other code (eg - // apollo-engine-reporting) assumes that. In fact, errors talking to backends - // are unlikely to show up as GraphQLErrors. Do we need to use - // formatApolloErrors or something? - public executor = async ( - requestContext: GraphQLRequestContextExecutionDidStart, - ): Promise => { - const { request, document, queryHash } = requestContext; - const queryPlanStoreKey = queryHash + (request.operationName || ''); - const operationContext = buildOperationContext( - this.schema!, - document, - request.operationName, - ); - - // No need to build a query plan if we know the request is invalid beforehand - // In the future, this should be controlled by the requestPipeline - const validationErrors = this.validateIncomingRequest( - requestContext, - operationContext, - ); - - if (validationErrors.length > 0) { - return { errors: validationErrors }; - } - - let queryPlan: QueryPlan | undefined; - if (this.queryPlanStore) { - queryPlan = await this.queryPlanStore.get(queryPlanStoreKey); - } - - if (!queryPlan) { - queryPlan = buildQueryPlan(operationContext, { - autoFragmentization: Boolean( - this.config.experimental_autoFragmentization, - ), - }); - if (this.queryPlanStore) { - // The underlying cache store behind the `documentStore` returns a - // `Promise` which is resolved (or rejected), eventually, based on the - // success or failure (respectively) of the cache save attempt. While - // it's certainly possible to `await` this `Promise`, we don't care about - // whether or not it's successful at this point. We'll instead proceed - // to serve the rest of the request and just hope that this works out. - // If it doesn't work, the next request will have another opportunity to - // try again. Errors will surface as warnings, as appropriate. - // - // While it shouldn't normally be necessary to wrap this `Promise` in a - // `Promise.resolve` invocation, it seems that the underlying cache store - // is returning a non-native `Promise` (e.g. Bluebird, etc.). - Promise.resolve( - this.queryPlanStore.set(queryPlanStoreKey, queryPlan), - ).catch(err => - this.logger.warn( - 'Could not store queryPlan' + ((err && err.message) || err), - ), - ); - } - } - - const serviceMap: ServiceMap = Object.entries(this.serviceMap).reduce( - (serviceDataSources, [serviceName, { dataSource }]) => { - serviceDataSources[serviceName] = dataSource; - return serviceDataSources; - }, - Object.create(null) as ServiceMap, - ); - - if (this.experimental_didResolveQueryPlan) { - this.experimental_didResolveQueryPlan({ - queryPlan, - serviceMap, - requestContext, - operationContext, - }); - } - - const response = await executeQueryPlan( - queryPlan, - serviceMap, - requestContext, - operationContext, - ); - - const shouldShowQueryPlan = - this.config.__exposeQueryPlanExperimental && - request.http && - request.http.headers && - request.http.headers.get('Apollo-Query-Plan-Experimental'); - - // We only want to serialize the query plan if we're going to use it, which is - // in two cases: - // 1) non-empty query plan and config.debug === true - // 2) non-empty query plan and shouldShowQueryPlan === true - const serializedQueryPlan = - queryPlan.node && (this.config.debug || shouldShowQueryPlan) - ? serializeQueryPlan(queryPlan) - : null; - - if (this.config.debug && serializedQueryPlan) { - this.logger.debug(serializedQueryPlan); - } - - if (shouldShowQueryPlan) { - // TODO: expose the query plan in a more flexible JSON format in the future - // and rename this to `queryPlan`. Playground should cutover to use the new - // option once we've built a way to print that representation. - - // In the case that `serializedQueryPlan` is null (on introspection), we - // still want to respond to Playground with something truthy since it depends - // on this to decide that query plans are supported by this gateway. - response.extensions = { - __queryPlanExperimental: serializedQueryPlan || true, - }; - } - return response; - }; - - protected validateIncomingRequest( - requestContext: GraphQLRequestContextExecutionDidStart, - operationContext: OperationContext, - ) { - // casting out of `readonly` - const variableDefinitions = operationContext.operation - .variableDefinitions as VariableDefinitionNode[] | undefined; - - if (!variableDefinitions) return []; - - const { errors } = getVariableValues( - operationContext.schema, - variableDefinitions, - requestContext.request.variables!, - ); - - return errors || []; - } - - private initializeQueryPlanStore(): void { - this.queryPlanStore = new InMemoryLRUCache({ - // Create ~about~ a 30MiB InMemoryLRUCache. This is less than precise - // since the technique to calculate the size of a DocumentNode is - // only using JSON.stringify on the DocumentNode (and thus doesn't account - // for unicode characters, etc.), but it should do a reasonable job at - // providing a caching document store for most operations. - maxSize: - Math.pow(2, 20) * - (this.experimental_approximateQueryPlanStoreMiB || 30), - sizeCalculator: approximateObjectSize, - }); - } - - public async stop() { - if (this.pollingTimer) { - clearTimeout(this.pollingTimer); - this.pollingTimer = undefined; - } - } -} - -function approximateObjectSize(obj: T): number { - return Buffer.byteLength(JSON.stringify(obj), 'utf8'); -} - -// We can't use transformSchema here because the extension data for query -// planning would be lost. Instead we set a resolver for each field -// in order to counteract GraphQLExtensions preventing a defaultFieldResolver -// from doing the same job -function wrapSchemaWithAliasResolver(schema: GraphQLSchema): GraphQLSchema { - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach(typeName => { - const type = typeMap[typeName]; - - if (isObjectType(type) && !isIntrospectionType(type)) { - const fields = type.getFields(); - Object.keys(fields).forEach(fieldName => { - const field = fields[fieldName]; - field.resolve = defaultFieldResolverWithAliasSupport; - }); - } - }); - return schema; -} - -export { - buildQueryPlan, - executeQueryPlan, - serializeQueryPlan, - buildOperationContext, - QueryPlan, - ServiceMap, -}; -export * from './datasources'; diff --git a/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts b/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts deleted file mode 100644 index c4dd8b9f3c8..00000000000 --- a/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { GraphQLRequest } from 'apollo-server-types'; -import { parse } from 'graphql'; -import { Headers, HeadersInit } from 'node-fetch'; -import { GraphQLDataSource } from './datasources/types'; -import { Experimental_UpdateServiceDefinitions, SERVICE_DEFINITION_QUERY } from './'; -import { ServiceDefinition } from '@apollo/federation'; - -export async function getServiceDefinitionsFromRemoteEndpoint({ - serviceList, - headers = {}, - serviceSdlCache, -}: { - serviceList: { - name: string; - url?: string; - dataSource: GraphQLDataSource; - }[]; - headers?: HeadersInit; - serviceSdlCache: Map; -}): ReturnType { - if (!serviceList || !serviceList.length) { - throw new Error( - 'Tried to load services from remote endpoints but none provided', - ); - } - - let isNewSchema = false; - // for each service, fetch its introspection schema - const promiseOfServiceList = serviceList.map(({ name, url, dataSource }) => { - if (!url) { - throw new Error( - `Tried to load schema for '${name}' but no 'url' was specified.`); - } - - const request: GraphQLRequest = { - query: SERVICE_DEFINITION_QUERY, - http: { - url, - method: 'POST', - headers: new Headers(headers), - }, - }; - - return dataSource - .process({ request, context: {} }) - .then(({ data, errors }): ServiceDefinition => { - if (data && !errors) { - const typeDefs = data._service.sdl as string; - const previousDefinition = serviceSdlCache.get(name); - // this lets us know if any downstream service has changed - // and we need to recalculate the schema - if (previousDefinition !== typeDefs) { - isNewSchema = true; - } - serviceSdlCache.set(name, typeDefs); - return { - name, - url, - typeDefs: parse(typeDefs), - }; - } - - throw new Error(errors?.map(e => e.message).join("\n")); - }) - .catch(err => { - const errorMessage = - `Couldn't load service definitions for "${name}" at ${url}` + - (err && err.message ? ": " + err.message || err : ""); - - throw new Error(errorMessage); - }); - }); - - const serviceDefinitions = await Promise.all(promiseOfServiceList); - return { serviceDefinitions, isNewSchema } -} diff --git a/packages/apollo-gateway/src/loadServicesFromStorage.ts b/packages/apollo-gateway/src/loadServicesFromStorage.ts deleted file mode 100644 index 08ada2fdff5..00000000000 --- a/packages/apollo-gateway/src/loadServicesFromStorage.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { fetch } from 'apollo-server-env'; -import { parse } from 'graphql'; -import { Experimental_UpdateServiceDefinitions } from '.'; - -interface LinkFileResult { - configPath: string; - formatVersion: number; -} - -interface ImplementingService { - formatVersion: number; - graphID: string; - graphVariant: string; - name: string; - revision: string; - url: string; - partialSchemaPath: string; -} - -interface ImplementingServiceLocation { - name: string; - path: string; -} - -export interface CompositionMetadata { - formatVersion: number; - id: string; - implementingServiceLocations: ImplementingServiceLocation[]; - schemaHash: string; -} - -const envOverridePartialSchemaBaseUrl = 'APOLLO_PARTIAL_SCHEMA_BASE_URL'; -const envOverrideStorageSecretBaseUrl = 'APOLLO_STORAGE_SECRET_BASE_URL'; - -const urlFromEnvOrDefault = (envKey: string, fallback: string) => - (process.env[envKey] || fallback).replace(/\/$/, ''); - -// Generate and cache our desired operation manifest URL. -const urlPartialSchemaBase = urlFromEnvOrDefault( - envOverridePartialSchemaBaseUrl, - 'https://federation.api.apollographql.com/', -); - -const urlStorageSecretBase: string = urlFromEnvOrDefault( - envOverrideStorageSecretBaseUrl, - 'https://storage-secrets.api.apollographql.com/', -); - -function getStorageSecretUrl(graphId: string, apiKeyHash: string): string { - return `${urlStorageSecretBase}/${graphId}/storage-secret/${apiKeyHash}.json`; -} - -function fetchApolloGcs( - fetcher: typeof fetch, - ...args: Parameters -): ReturnType { - const [input, init] = args; - - // Used in logging. - const url = typeof input === 'object' && input.url || input; - - return fetcher(input, init) - .catch(fetchError => { - throw new Error( - "Cannot access Apollo Graph Manager storage: " + fetchError) - }) - .then(async (response) => { - // If the fetcher has a cache and has implemented ETag validation, then - // a 304 response may be returned. Either way, we will return the - // non-JSON-parsed version and let the caller decide if that's important - // to their needs. - if (response.ok || response.status === 304) { - return response; - } - - // We won't make any assumptions that the body is anything but text, to - // avoid parsing errors in this unknown condition. - const body = await response.text(); - - // Google Cloud Storage returns an `application/xml` error under error - // conditions. We'll special-case our known errors, and resort to - // printing the body for others. - if ( - response.headers.get('content-type') === 'application/xml' && - response.status === 403 && - body.includes("AccessDenied") && - body.includes("Anonymous caller does not have storage.objects.get") - ) { - throw new Error( - "Unable to authenticate with Apollo Graph Manager storage " + - "while fetching " + url + ". Ensure that the API key is " + - "configured properly and that a federated service has been " + - "pushed. For details, see " + - "https://go.apollo.dev/g/resolve-access-denied."); - } - - // Normally, we'll try to keep the logs clean with errors we expect. - // If it's not a known error, reveal the full body for debugging. - throw new Error( - "Could not communicate with Apollo Graph Manager storage: " + body); - }); -}; - -export async function getServiceDefinitionsFromStorage({ - graphId, - apiKeyHash, - graphVariant, - federationVersion, - fetcher, -}: { - graphId: string; - apiKeyHash: string; - graphVariant?: string; - federationVersion: number; - fetcher: typeof fetch; -}): ReturnType { - // fetch the storage secret - const storageSecretUrl = getStorageSecretUrl(graphId, apiKeyHash); - - // The storage secret is a JSON string (e.g. `"secret"`). - const secret: string = - await fetchApolloGcs(fetcher, storageSecretUrl).then(res => res.json()); - - if (!graphVariant) { - graphVariant = 'current'; - } - - const baseUrl = `${urlPartialSchemaBase}/${secret}/${graphVariant}/v${federationVersion}`; - - const compositionConfigResponse = - await fetchApolloGcs(fetcher, `${baseUrl}/composition-config-link`); - - if (compositionConfigResponse.status === 304) { - return { isNewSchema: false }; - } - - const linkFileResult: LinkFileResult = await compositionConfigResponse.json(); - - const compositionMetadata: CompositionMetadata = await fetchApolloGcs( - fetcher, - `${urlPartialSchemaBase}/${linkFileResult.configPath}`, - ).then(res => res.json()); - - // It's important to maintain the original order here - const serviceDefinitions = await Promise.all( - compositionMetadata.implementingServiceLocations.map( - async ({ name, path }) => { - const { url, partialSchemaPath }: ImplementingService = await fetcher( - `${urlPartialSchemaBase}/${path}`, - ).then(response => response.json()); - - const sdl = await fetcher( - `${urlPartialSchemaBase}/${partialSchemaPath}`, - ).then(response => response.text()); - - return { name, url, typeDefs: parse(sdl) }; - }, - ), - ); - - // explicity return that this is a new schema, as the link file has changed. - // we can't use the hit property of the fetchPartialSchemaFiles, as the partial - // schema may all be cache hits with the final schema still being new - // (for instance if a partial schema is removed or a partial schema is rolled back to a prior version, which is still in cache) - return { - serviceDefinitions, - compositionMetadata, - isNewSchema: true, - }; -} diff --git a/packages/apollo-gateway/src/make-fetch-happen.d.ts b/packages/apollo-gateway/src/make-fetch-happen.d.ts deleted file mode 100644 index 6b7f1737a10..00000000000 --- a/packages/apollo-gateway/src/make-fetch-happen.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * We are attempting to get types included natively in this package, but it - * has not happened, yet! - * - * See https://github.com/npm/make-fetch-happen/issues/20 - */ -declare module 'make-fetch-happen' { - import { - Response, - Request, - RequestInfo, - RequestInit, - } from 'apollo-server-env'; - - // If adding to these options, they should mirror those from `make-fetch-happen` - // @see: https://github.com/npm/make-fetch-happen/#extra-options - export interface FetcherOptions { - cacheManager?: string | CacheManager; - // @see: https://www.npmjs.com/package/retry#retrytimeoutsoptions - retry?: - | boolean - | number - | { - // The maximum amount of times to retry the operation. Default is 10. Seting this to 1 means do it once, then retry it once - retries?: number; - // The exponential factor to use. Default is 2. - factor?: number; - // The number of milliseconds before starting the first retry. Default is 1000. - minTimeout?: number; - // The maximum number of milliseconds between two retries. Default is Infinity. - maxTimeout?: number; - // Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false. - randomize?: boolean; - }; - onRetry?(): void; - } - - export interface CacheManager { - delete(req: Request): Promise; - put(req: Request, res: Response): Promise; - match(req: Request): Promise; - } - - /** - * This is an augmentation of the fetch function types provided by `apollo-server-env` - * @see: https://git.io/JvBwX - */ - export interface Fetcher { - (input?: RequestInfo, init?: RequestInit & FetcherOptions): Promise< - Response - >; - } - - let fetch: Fetcher & { - defaults(opts?: RequestInit & FetcherOptions): Fetcher; - }; - - export default fetch; -} diff --git a/packages/apollo-gateway/src/snapshotSerializers/astSerializer.ts b/packages/apollo-gateway/src/snapshotSerializers/astSerializer.ts deleted file mode 100644 index f3cee95e79d..00000000000 --- a/packages/apollo-gateway/src/snapshotSerializers/astSerializer.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { ASTNode, print, Kind, visit } from 'graphql'; -import { Plugin, Config, Refs } from 'pretty-format'; -import { QueryPlanSelectionNode, QueryPlanInlineFragmentNode } from '../QueryPlan'; -import { SelectionNode as GraphQLJSSelectionNode } from 'graphql'; - -export default { - test(value: any) { - return value && typeof value.kind === 'string'; - }, - - serialize( - value: ASTNode, - _config: Config, - indentation: string, - _depth: number, - _refs: Refs, - _printer: any, - ): string { - return print(remapInlineFragmentNodes(value)) - .trim() - .replace(/\n\n/g, '\n') - .replace(/\n/g, '\n' + indentation); - }, -} as Plugin; - -/** - * This function converts potential InlineFragmentNodes that WE created - * (defined in ../QueryPlan, not graphql-js) to GraphQL-js compliant AST nodes - * for the graphql-js printer to work with - * - * The arg type here SHOULD be (node: AstNode | SelectionNode (from ../QueryPlan)), - * but that breaks the graphql-js visitor, as it won't allow our redefined - * SelectionNode to be passed in. - * - * Since our SelectionNode still has a `kind`, this will still functionally work - * at runtime to call the InlineFragment visitor defined below - * - * We have to cast the `fragmentNode as unknown` and then to an InlineFragmentNode - * at the bottom though, since there's no way to cast it appropriately to an - * `InlineFragmentNode` as defined in ../QueryPlan.ts. TypeScript will complain - * about there not being overlapping fields - */ -export function remapInlineFragmentNodes(node: ASTNode): ASTNode { - return visit(node, { - InlineFragment: (fragmentNode) => { - // if the fragmentNode is already a proper graphql AST Node, return it - if (fragmentNode.selectionSet) return fragmentNode; - - /** - * Since the above check wasn't hit, we _know_ that fragmentNode is an - * InlineFragmentNode from ../QueryPlan, but we can't actually type that - * without causing ourselves a lot of headache, so we cast to unknown and - * then to InlineFragmentNode (from ../QueryPlan) below - */ - - // if the fragmentNode is a QueryPlan InlineFragmentNode, convert it to graphql-js node - return { - kind: Kind.INLINE_FRAGMENT, - typeCondition: fragmentNode.typeCondition - ? { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: fragmentNode.typeCondition, - }, - } - : undefined, - selectionSet: { - kind: Kind.SELECTION_SET, - // we have to recursively rebuild the selectionSet using selections - selections: remapSelections( - ((fragmentNode as unknown) as QueryPlanInlineFragmentNode).selections, - ), - }, - }; - }, - }); -} - -function remapSelections( - selections: QueryPlanSelectionNode[], -): ReadonlyArray { - return selections.map((selection) => { - switch (selection.kind) { - case Kind.FIELD: - return { - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: selection.name, - }, - selectionSet: { - kind: Kind.SELECTION_SET, - selections: remapSelections(selection.selections || []), - }, - }; - case Kind.INLINE_FRAGMENT: - return { - kind: Kind.INLINE_FRAGMENT, - selectionSet: { - kind: Kind.SELECTION_SET, - selections: remapSelections(selection.selections || []), - }, - typeCondition: selection.typeCondition - ? { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: selection.typeCondition, - }, - } - : undefined, - }; - } - }); -} diff --git a/packages/apollo-gateway/src/snapshotSerializers/index.ts b/packages/apollo-gateway/src/snapshotSerializers/index.ts deleted file mode 100644 index 3a768cfc197..00000000000 --- a/packages/apollo-gateway/src/snapshotSerializers/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import astSerializer from './astSerializer'; -import selectionSetSerializer from './selectionSetSerializer'; -import typeSerializer from './typeSerializer'; -import queryPlanSerializer from './queryPlanSerializer'; -export { - astSerializer, - selectionSetSerializer, - typeSerializer, - queryPlanSerializer, -}; - -declare global { - namespace jest { - interface Expect { - /** - * Adds a module to format application-specific data structures for serialization. - */ - addSnapshotSerializer(serializer: import('pretty-format').Plugin): void; - } - } -} diff --git a/packages/apollo-gateway/src/snapshotSerializers/queryPlanSerializer.ts b/packages/apollo-gateway/src/snapshotSerializers/queryPlanSerializer.ts deleted file mode 100644 index 08023fe2e44..00000000000 --- a/packages/apollo-gateway/src/snapshotSerializers/queryPlanSerializer.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Config, Plugin, Refs } from 'pretty-format'; -import { PlanNode, QueryPlan } from '../QueryPlan'; -import { parse, Kind, visit, DocumentNode } from 'graphql'; - -export default { - test(value: any) { - return value && value.kind === 'QueryPlan'; - }, - - serialize( - queryPlan: QueryPlan, - config: Config, - indentation: string, - depth: number, - refs: Refs, - printer: any, - ): string { - return ( - 'QueryPlan {' + - printNodes( - queryPlan.node ? [queryPlan.node] : undefined, - config, - indentation, - depth, - refs, - printer, - ) + - '}' - ); - }, -} as Plugin; - -function printNode( - node: PlanNode, - config: Config, - indentation: string, - depth: number, - refs: Refs, - printer: any, -): string { - let result = ''; - - const indentationNext = indentation + config.indent; - - switch (node.kind) { - case 'Fetch': - result += - `Fetch(service: "${node.serviceName}")` + - ' {' + - config.spacingOuter + - indentationNext + - (node.requires - ? printer( - // this is an array of selections, so we need to make it a proper - // selectionSet so we can print it - { kind: Kind.SELECTION_SET, selections: node.requires }, - config, - indentationNext, - depth, - refs, - printer, - ) + - ' =>' + - config.spacingOuter + - indentationNext - : '') + - printer( - flattenEntitiesField(parse(node.operation)), - config, - indentationNext, - depth, - refs, - printer, - ) + - config.spacingOuter + - indentation + - '}'; - break; - case 'Flatten': - result += `Flatten(path: "${node.path.join('.')}")`; - break; - default: - result += node.kind; - } - - const nodes = - 'nodes' in node ? node.nodes : 'node' in node ? [node.node] : []; - - if (nodes.length > 0) { - result += - ' {' + printNodes(nodes, config, indentation, depth, refs, printer) + '}'; - } - - return result; -} - -function printNodes( - nodes: PlanNode[] | undefined, - config: Config, - indentation: string, - depth: number, - refs: Refs, - printer: any, -): string { - let result = ''; - - if (nodes && nodes.length > 0) { - result += config.spacingOuter; - - const indentationNext = indentation + config.indent; - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (!node) continue; - - result += - indentationNext + - printNode(node, config, indentationNext, depth, refs, printer); - - if (i < nodes.length - 1) { - result += ',' + config.spacingInner; - } else if (!config.min) { - result += ','; - } - } - - result += config.spacingOuter + indentation; - } - - return result; -} - -/** - * when we serialize a query plan, we want to serialize the operation, but not - * show the root level `query` definition or the `_entities` call. This function - * flattens those nodes to only show their selectionSets - */ -function flattenEntitiesField(node: DocumentNode) { - return visit(node, { - OperationDefinition: ({ operation, selectionSet }) => { - const firstSelection = selectionSet.selections[0]; - if ( - operation === 'query' && - firstSelection.kind === Kind.FIELD && - firstSelection.name.value === '_entities' - ) { - return firstSelection.selectionSet; - } - // we don't want to print the `query { }` definition either for query plan printing - return selectionSet; - }, - }); -} diff --git a/packages/apollo-gateway/src/snapshotSerializers/selectionSetSerializer.ts b/packages/apollo-gateway/src/snapshotSerializers/selectionSetSerializer.ts deleted file mode 100644 index 33dfba417ac..00000000000 --- a/packages/apollo-gateway/src/snapshotSerializers/selectionSetSerializer.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { print, SelectionNode, isSelectionNode } from 'graphql'; -import { Plugin } from 'pretty-format'; - -export default { - test(value: any) { - return ( - Array.isArray(value) && value.length > 0 && value.every(isSelectionNode) - ); - }, - print(selectionNodes: SelectionNode[]): string { - return selectionNodes.map(node => print(node)).join('\n'); - }, -} as Plugin; diff --git a/packages/apollo-gateway/src/snapshotSerializers/typeSerializer.ts b/packages/apollo-gateway/src/snapshotSerializers/typeSerializer.ts deleted file mode 100644 index 7b78e18cac1..00000000000 --- a/packages/apollo-gateway/src/snapshotSerializers/typeSerializer.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { isNamedType, GraphQLNamedType, printType } from 'graphql'; -import { Plugin } from 'pretty-format'; - -export default { - test(value: any) { - return value && isNamedType(value); - }, - print(value: GraphQLNamedType) { - return printType(value); - }, -} as Plugin; diff --git a/packages/apollo-gateway/src/utilities/MultiMap.ts b/packages/apollo-gateway/src/utilities/MultiMap.ts deleted file mode 100644 index e29908288dc..00000000000 --- a/packages/apollo-gateway/src/utilities/MultiMap.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class MultiMap extends Map { - add(key: K, value: V): this { - let values = this.get(key); - if (values) { - values.push(value); - } else { - this.set(key, (values = [value])); - } - return this; - } -} diff --git a/packages/apollo-gateway/src/utilities/__tests__/deepMerge.test.ts b/packages/apollo-gateway/src/utilities/__tests__/deepMerge.test.ts deleted file mode 100644 index 545968ba91a..00000000000 --- a/packages/apollo-gateway/src/utilities/__tests__/deepMerge.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { deepMerge } from '../deepMerge'; - -describe('deepMerge', () => { - it('merges basic', () => { - const target = { - a: 1, - b: 2, - }; - - const source = { - b: 3, - c: 4, - }; - - expect(deepMerge(target, source)).toEqual({ - a: 1, - b: 3, - c: 4, - }); - }); - - it('merges nested objects', () => { - const target = { - a: 1, - b: { - someProperty: 1, - overwrittenProperty: 'clean', - }, - }; - - const source = { - b: { - overwrittenProperty: 'dirty', - newProperty: 'new', - }, - c: 4, - }; - - expect(deepMerge(target, source)).toEqual({ - a: 1, - b: { - newProperty: 'new', - overwrittenProperty: 'dirty', - someProperty: 1, - }, - c: 4, - }); - }); - - it('ignores merging __proto__ fields', () => { - const target = {}; - - // Bypass setters on __proto__ - const source = JSON.parse('{"__proto__": {"pollution": true}}'); - deepMerge(target, source); - - expect(Object.prototype.hasOwnProperty('pollution')).toBe(false); - }); - - it('merges arrays', () => { - const target = { - a: 1, - b: [{ c: 1, d: 2 }], - }; - - const source = { - e: 2, - b: [{ f: 3 }], - }; - - expect(deepMerge(target, source)).toEqual({ - a: 1, - e: 2, - b: [{ c: 1, d: 2, f: 3 }], - }); - }); -}); diff --git a/packages/apollo-gateway/src/utilities/array.ts b/packages/apollo-gateway/src/utilities/array.ts deleted file mode 100644 index 2aaea1d5e9c..00000000000 --- a/packages/apollo-gateway/src/utilities/array.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { isNotNullOrUndefined } from 'apollo-env'; - -export function compactMap( - array: T[], - callbackfn: (value: T, index: number, array: T[]) => U | null | undefined, -): U[] { - return array.reduce( - (accumulator, element, index, array) => { - const result = callbackfn(element, index, array); - if (isNotNullOrUndefined(result)) { - accumulator.push(result); - } - return accumulator; - }, - [] as U[], - ); -} - -export function partition( - array: T[], - predicate: (element: T, index: number, array: T[]) => element is U, -): [U[], T[]]; -export function partition( - array: T[], - predicate: (element: T, index: number, array: T[]) => boolean, -): [T[], T[]]; -export function partition( - array: T[], - predicate: (element: T, index: number, array: T[]) => boolean, -): [T[], T[]] { - array.map; - return array.reduce( - (accumulator, element, index) => { - return ( - predicate(element, index, array) - ? accumulator[0].push(element) - : accumulator[1].push(element), - accumulator - ); - }, - [[], []] as [T[], T[]], - ); -} - -export function findAndExtract( - array: T[], - predicate: (element: T, index: number, array: T[]) => boolean, -): [T | undefined, T[]] { - const index = array.findIndex(predicate); - if (index === -1) return [undefined, array]; - - let remaining = array.slice(0, index); - if (index < array.length - 1) { - remaining.push(...array.slice(index + 1)); - } - - return [array[index], remaining]; -} - -export function groupBy(keyFunction: (element: T) => U) { - return (iterable: Iterable) => { - const result = new Map(); - - for (const element of iterable) { - const key = keyFunction(element); - const group = result.get(key); - - if (group) { - group.push(element); - } else { - result.set(key, [element]); - } - } - - return result; - }; -} diff --git a/packages/apollo-gateway/src/utilities/deepMerge.ts b/packages/apollo-gateway/src/utilities/deepMerge.ts deleted file mode 100644 index fb5504a38a2..00000000000 --- a/packages/apollo-gateway/src/utilities/deepMerge.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { isObject } from './predicates'; - -export function deepMerge(target: any, source: any): any { - if (source === undefined || source === null) return target; - - for (const key of Object.keys(source)) { - if (source[key] === undefined || key === '__proto__') continue; - - if (target[key] && isObject(source[key])) { - deepMerge(target[key], source[key]); - } else if ( - Array.isArray(source[key]) && - Array.isArray(target[key]) && - source[key].length === target[key].length - ) { - let i = 0; - for (; i < source[key].length; i++) { - if (isObject(target[key][i]) && isObject(source[key][i])) { - deepMerge(target[key][i], source[key][i]); - } else { - target[key][i] = source[key][i]; - } - } - } else { - target[key] = source[key]; - } - } - - return target; -} diff --git a/packages/apollo-gateway/src/utilities/graphql.ts b/packages/apollo-gateway/src/utilities/graphql.ts deleted file mode 100644 index d222c9dcaf8..00000000000 --- a/packages/apollo-gateway/src/utilities/graphql.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - ASTNode, - FieldNode, - GraphQLCompositeType, - GraphQLField, - GraphQLInterfaceType, - GraphQLNullableType, - GraphQLObjectType, - GraphQLSchema, - GraphQLType, - GraphQLUnionType, - isListType, - isNonNullType, - Kind, - ListTypeNode, - NamedTypeNode, - OperationDefinitionNode, - parse, - print, - SchemaMetaFieldDef, - SelectionNode, - TypeMetaFieldDef, - TypeNameMetaFieldDef, - TypeNode, -} from 'graphql'; - -/** - * Not exactly the same as the executor's definition of getFieldDef, in this - * statically evaluated environment we do not always have an Object type, - * and need to handle Interface and Union types. - */ -export function getFieldDef( - schema: GraphQLSchema, - parentType: GraphQLCompositeType, - fieldName: string, -): GraphQLField | undefined { - if ( - fieldName === SchemaMetaFieldDef.name && - schema.getQueryType() === parentType - ) { - return SchemaMetaFieldDef; - } - if ( - fieldName === TypeMetaFieldDef.name && - schema.getQueryType() === parentType - ) { - return TypeMetaFieldDef; - } - if ( - fieldName === TypeNameMetaFieldDef.name && - (parentType instanceof GraphQLObjectType || - parentType instanceof GraphQLInterfaceType || - parentType instanceof GraphQLUnionType) - ) { - return TypeNameMetaFieldDef; - } - if ( - parentType instanceof GraphQLObjectType || - parentType instanceof GraphQLInterfaceType - ) { - return parentType.getFields()[fieldName]; - } - - return undefined; -} - -export function getResponseName(node: FieldNode): string { - return node.alias ? node.alias.value : node.name.value; -} - -export function allNodesAreOfSameKind( - firstNode: T, - remainingNodes: ASTNode[], -): remainingNodes is T[] { - return !remainingNodes.some(node => node.kind !== firstNode.kind); -} - -export function astFromType( - type: GraphQLNullableType, -): NamedTypeNode | ListTypeNode; -export function astFromType(type: GraphQLType): TypeNode { - if (isListType(type)) { - return { kind: Kind.LIST_TYPE, type: astFromType(type.ofType) }; - } else if (isNonNullType(type)) { - return { kind: Kind.NON_NULL_TYPE, type: astFromType(type.ofType) }; - } else { - return { - kind: Kind.NAMED_TYPE, - name: { kind: Kind.NAME, value: type.name }, - }; - } -} - -export function printWithReducedWhitespace(ast: ASTNode): string { - return print(ast) - .replace(/\s+/g, ' ') - .trim(); -} - -export function parseSelections(source: string): ReadonlyArray { - return (parse(`query { ${source} }`) - .definitions[0] as OperationDefinitionNode).selectionSet.selections; -} diff --git a/packages/apollo-gateway/src/utilities/predicates.ts b/packages/apollo-gateway/src/utilities/predicates.ts deleted file mode 100644 index d9a73b8e7e5..00000000000 --- a/packages/apollo-gateway/src/utilities/predicates.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function isObject(value: any): value is object { - return ( - value !== undefined && - value !== null && - typeof value === 'object' && - !Array.isArray(value) - ); -} diff --git a/packages/apollo-gateway/tsconfig.json b/packages/apollo-gateway/tsconfig.json deleted file mode 100644 index d2773072fdb..00000000000 --- a/packages/apollo-gateway/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.base", - "compilerOptions": { - "target": "es2019", - "rootDir": "./src", - "outDir": "./dist" - }, - "include": ["src/**/*"], - "exclude": ["**/__tests__", "**/__mocks__"], - "references": [ - { "path": "../apollo-server-core" }, - { "path": "../apollo-server-types" }, - { "path": "../apollo-federation" }, - { "path": "../apollo-federation-integration-testsuite" }, - { "path": "../graphql-extensions" }, - ] -} From 092624c584c01edec59040ec827e708e5f72e00a Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 11 Sep 2020 14:38:23 +0000 Subject: [PATCH 3/4] Update the `CHANGELOG.md` to reflect the new home for Federation/Gateway. --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af62c8335ab..23746ec442e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # CHANGELOG -The version headers in this history reflect the versions of Apollo Server itself. Versions of other packages (e.g. which are not actual HTTP integrations; packages not prefixed with `apollo-server`) may use different versions. For more details, check the publish commit for that version in the Git history, or check the individual CHANGELOGs for specific packages which are maintained separately: +The version headers in this history reflect the versions of Apollo Server itself. Versions of other packages (e.g., those which are not actual HTTP integrations; packages not prefixed with "`apollo-server`", or just supporting packages) may use different versions. -- [__CHANGELOG for `@apollo/gateway`__](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/CHANGELOG.md) -- [__CHANGELOG for `@apollo/federation`__](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-federation/CHANGELOG.md) +🆕 **Please Note!**: 🆕 **The `@apollo/federation` and `@apollo/gateway` packages now live in the [`apollographql/federation`](https://github.com/apollographql/federation) repository.** + +- [`@apollo/gateway`](https://github.com/apollographql/federation/blob/HEAD/gateway-js/CHANGELOG.md) +- [`@apollo/federation`](https://github.com/apollographql/federation/blob/HEAD/federation-js/CHANGELOG.md) ## vNEXT From 85c70a441e044c8ae0256c8ba7f1fb5ffb7affc8 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 11 Sep 2020 16:35:05 +0000 Subject: [PATCH 4/4] Remove `apollo-federation` and `apollo-gateway` from Jest module not-mapping. --- jest.config.base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.base.js b/jest.config.base.js index d033e9227a0..328505f1da7 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -21,7 +21,7 @@ module.exports = { // We don't want to match `apollo-server-env` and // `apollo-engine-reporting-protobuf`, because these don't depend on // compilation but need to be initialized from as parto of `prepare`. - '^(?!apollo-server-env|apollo-engine-reporting-protobuf)(apollo-(?:federation|gateway|server|datasource|cache-control|tracing|engine)[^/]*|graphql-extensions)(?:/dist)?((?:/.*)|$)': '/../../packages/$1/src$2' + '^(?!apollo-server-env|apollo-engine-reporting-protobuf)(apollo-(?:server|datasource|cache-control|tracing|engine)[^/]*|graphql-extensions)(?:/dist)?((?:/.*)|$)': '/../../packages/$1/src$2' }, clearMocks: true, globals: {