From 88d623289e187435ddc88bbe3f4623a727101207 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 13 Jun 2024 18:04:51 +0300 Subject: [PATCH] Refactor the runtime (#7054) * Refactor the runtime * Update versions * chore(dependencies): updated changesets for modified dependencies * Fix e2e tests * Disposable executor * chore(dependencies): updated changesets for modified dependencies * Hold connections per context per source * Go * ESM instead of CJS * chore(dependencies): updated changesets for modified dependencies * Yarn.lock * More refactor * Cleanup * chore(dependencies): updated changesets for modified dependencies * Refactor and clean the logs in the unit tests --------- Co-authored-by: github-actions[bot] --- ...l-mesh_fusion-runtime-7009-dependencies.md | 1 + ...l-mesh_fusion-runtime-7054-dependencies.md | 6 + ...raphql-mesh_serve-cli-7054-dependencies.md | 5 + ...ql-mesh_serve-runtime-7009-dependencies.md | 5 + ...ql-mesh_serve-runtime-7054-dependencies.md | 5 + .../@graphql-mesh_utils-7054-dependencies.md | 5 + .changeset/fair-knives-float.md | 6 + .changeset/modern-dolls-retire.md | 6 + .changeset/poor-bikes-sleep.md | 21 + .changeset/young-keys-heal.md | 7 + .github/workflows/loadtest.yml | 2 - .github/workflows/pr.yml | 2 - .github/workflows/tests.yml | 2 - .github/workflows/website.yml | 2 - .prettierrc.cjs | 4 + babel.config.js | 5 +- declarations.d.ts | 5 + e2e/json-schema-subscriptions/mesh.config.ts | 1 - e2e/mysql-rfam/mesh.config.ts | 7 - e2e/neo4j-example/mesh.config.ts | 7 - e2e/openapi-javascript-wiki/mesh.config.ts | 1 - e2e/openapi-subscriptions/mesh.config.ts | 1 - e2e/pubsub-destroy/CHANGELOG.md | 101 ----- e2e/pubsub-destroy/mesh.config.ts | 16 - e2e/pubsub-destroy/package.json | 8 - e2e/pubsub-destroy/pubsub-destroy.test.ts | 8 - examples/grpc-example/tests/grpc.test.ts | 1 - jest.config.js | 3 +- package.json | 1 + .../redis/__integration_tests__/redis.spec.ts | 15 +- packages/cache/redis/src/index.ts | 11 +- packages/cache/redis/test/cache.spec.ts | 34 +- packages/compose-cli/package.json | 2 +- packages/compose-cli/src/run.ts | 23 +- packages/fusion/runtime/package.json | 2 + packages/fusion/runtime/src/federation.ts | 55 +++ .../src/getSubschemasFromFusiongraph.ts | 67 ++- packages/fusion/runtime/src/index.ts | 3 +- .../fusion/runtime/src/unifiedGraphManager.ts | 234 +++++++++++ packages/fusion/runtime/src/useFusiongraph.ts | 290 ------------- packages/fusion/runtime/src/utils.ts | 382 +++++++++++------- packages/fusion/runtime/tests/polling.test.ts | 43 ++ .../tests/transforms/hoist-field.test.ts | 2 +- .../transforms/naming-convention.test.ts | 10 +- packages/fusion/runtime/tests/utils.ts | 83 ++-- packages/legacy/handlers/mysql/src/index.ts | 20 +- packages/legacy/handlers/neo4j/src/index.ts | 5 + packages/legacy/handlers/soap/src/index.ts | 1 + .../legacy/handlers/supergraph/src/index.ts | 7 +- packages/legacy/runtime/src/get-mesh.ts | 51 +-- packages/legacy/runtime/src/in-context-sdk.ts | 79 ++-- packages/legacy/runtime/src/useSubschema.ts | 92 +++-- packages/legacy/runtime/test/getMesh.test.ts | 14 +- packages/legacy/types/src/index.ts | 2 +- packages/legacy/utils/package.json | 1 + packages/legacy/utils/src/iterateAsync.ts | 22 +- .../utils/src/registerTerminateHandler.ts | 12 + .../legacy/utils/src/wrapFetchWithHooks.ts | 98 +++-- packages/loaders/neo4j/src/index.ts | 5 +- packages/loaders/neo4j/src/schema.ts | 1 - packages/loaders/soap/src/SOAPLoader.ts | 15 +- packages/loaders/soap/src/index.ts | 6 +- packages/loaders/soap/test/examples.test.ts | 10 + packages/loaders/soap/test/soap.test.ts | 13 +- packages/plugins/hive/src/index.ts | 5 + packages/plugins/newrelic/src/index.ts | 14 +- .../prometheus/tests/prometheus.spec.ts | 1 + packages/serve-cli/package.json | 8 +- packages/serve-cli/src/nodeHttp.ts | 41 +- packages/serve-cli/src/run.ts | 47 ++- packages/serve-cli/src/uWebSockets.ts | 14 +- packages/serve-runtime/package.json | 1 + .../serve-runtime/src/createServeRuntime.ts | 193 +++++---- .../serve-runtime/src/getProxyExecutor.ts | 41 +- .../src/handleUnifiedGraphConfig.ts | 14 +- packages/serve-runtime/src/types.ts | 10 +- .../src/useFederationSupergraph.ts | 168 -------- .../serve-runtime/tests/serve-runtime.spec.ts | 1 + .../tests/useForwardHeaders.spec.ts | 1 + packages/transports/common/src/types.ts | 2 + packages/transports/mysql/src/execution.ts | 46 +-- packages/transports/mysql/src/index.ts | 4 +- packages/transports/neo4j/src/executor.ts | 26 +- .../rest/src/directives/httpOperation.ts | 8 +- setup-jest.js | 3 + tsconfig.json | 3 +- yarn.lock | 96 ++++- 87 files changed, 1344 insertions(+), 1357 deletions(-) create mode 100644 .changeset/@graphql-mesh_fusion-runtime-7054-dependencies.md create mode 100644 .changeset/@graphql-mesh_serve-cli-7054-dependencies.md create mode 100644 .changeset/@graphql-mesh_serve-runtime-7009-dependencies.md create mode 100644 .changeset/@graphql-mesh_serve-runtime-7054-dependencies.md create mode 100644 .changeset/@graphql-mesh_utils-7054-dependencies.md create mode 100644 .changeset/fair-knives-float.md create mode 100644 .changeset/modern-dolls-retire.md create mode 100644 .changeset/poor-bikes-sleep.md create mode 100644 .changeset/young-keys-heal.md delete mode 100644 e2e/pubsub-destroy/CHANGELOG.md delete mode 100644 e2e/pubsub-destroy/mesh.config.ts delete mode 100644 e2e/pubsub-destroy/package.json delete mode 100644 e2e/pubsub-destroy/pubsub-destroy.test.ts create mode 100644 packages/fusion/runtime/src/federation.ts create mode 100644 packages/fusion/runtime/src/unifiedGraphManager.ts delete mode 100644 packages/fusion/runtime/src/useFusiongraph.ts create mode 100644 packages/fusion/runtime/tests/polling.test.ts delete mode 100644 packages/serve-runtime/src/useFederationSupergraph.ts create mode 100644 setup-jest.js diff --git a/.changeset/@graphql-mesh_fusion-runtime-7009-dependencies.md b/.changeset/@graphql-mesh_fusion-runtime-7009-dependencies.md index 2ca87a1056f06..69c6e5af1593e 100644 --- a/.changeset/@graphql-mesh_fusion-runtime-7009-dependencies.md +++ b/.changeset/@graphql-mesh_fusion-runtime-7009-dependencies.md @@ -4,3 +4,4 @@ dependencies updates: - Added dependency [`@envelop/core@^5.0.1` ↗︎](https://www.npmjs.com/package/@envelop/core/v/5.0.1) (to `dependencies`) - Added dependency [`@graphql-tools/executor@^1.2.6` ↗︎](https://www.npmjs.com/package/@graphql-tools/executor/v/1.2.6) (to `dependencies`) + - Added dependency [`@graphql-tools/federation@^1.1.36` ↗︎](https://www.npmjs.com/package/@graphql-tools/federation/v/1.1.36) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_fusion-runtime-7054-dependencies.md b/.changeset/@graphql-mesh_fusion-runtime-7054-dependencies.md new file mode 100644 index 0000000000000..9e0014eaece2f --- /dev/null +++ b/.changeset/@graphql-mesh_fusion-runtime-7054-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-mesh/fusion-runtime": patch +--- +dependencies updates: + - Added dependency [`@graphql-tools/federation@^2.0.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/federation/v/2.0.0) (to `dependencies`) + - Added dependency [`disposablestack@^1.1.6` ↗︎](https://www.npmjs.com/package/disposablestack/v/1.1.6) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_serve-cli-7054-dependencies.md b/.changeset/@graphql-mesh_serve-cli-7054-dependencies.md new file mode 100644 index 0000000000000..b8f88c2693701 --- /dev/null +++ b/.changeset/@graphql-mesh_serve-cli-7054-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/serve-cli": patch +--- +dependencies updates: + - Removed dependency [`uWebSockets.js@uNetworking/uWebSockets.js#semver:^20` ↗︎](https://www.npmjs.com/package/uWebSockets.js/v/20.0.0) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_serve-runtime-7009-dependencies.md b/.changeset/@graphql-mesh_serve-runtime-7009-dependencies.md new file mode 100644 index 0000000000000..1f4decf9b8f0a --- /dev/null +++ b/.changeset/@graphql-mesh_serve-runtime-7009-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/serve-runtime": patch +--- +dependencies updates: + - Removed dependency [`@graphql-tools/federation@^1.1.36` ↗︎](https://www.npmjs.com/package/@graphql-tools/federation/v/1.1.36) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_serve-runtime-7054-dependencies.md b/.changeset/@graphql-mesh_serve-runtime-7054-dependencies.md new file mode 100644 index 0000000000000..4aa0fa3270258 --- /dev/null +++ b/.changeset/@graphql-mesh_serve-runtime-7054-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/serve-runtime": patch +--- +dependencies updates: + - Added dependency [`disposablestack@^1.1.6` ↗︎](https://www.npmjs.com/package/disposablestack/v/1.1.6) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_utils-7054-dependencies.md b/.changeset/@graphql-mesh_utils-7054-dependencies.md new file mode 100644 index 0000000000000..99e6089f89670 --- /dev/null +++ b/.changeset/@graphql-mesh_utils-7054-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/utils": patch +--- +dependencies updates: + - Added dependency [`disposablestack@^1.1.6` ↗︎](https://www.npmjs.com/package/disposablestack/v/1.1.6) (to `dependencies`) diff --git a/.changeset/fair-knives-float.md b/.changeset/fair-knives-float.md new file mode 100644 index 0000000000000..0d0ae6ffd746a --- /dev/null +++ b/.changeset/fair-knives-float.md @@ -0,0 +1,6 @@ +--- +'@graphql-mesh/compose-cli': minor +'@graphql-mesh/serve-cli': minor +--- + +Use ESM instead of CommonJS in CLI diff --git a/.changeset/modern-dolls-retire.md b/.changeset/modern-dolls-retire.md new file mode 100644 index 0000000000000..76ee843b62093 --- /dev/null +++ b/.changeset/modern-dolls-retire.md @@ -0,0 +1,6 @@ +--- +'@graphql-mesh/transport-rest': patch +--- + +Do not consume the uploaded file inside the fetch call, and pass it to the upstream directly as a +stream diff --git a/.changeset/poor-bikes-sleep.md b/.changeset/poor-bikes-sleep.md new file mode 100644 index 0000000000000..d14157ccfffac --- /dev/null +++ b/.changeset/poor-bikes-sleep.md @@ -0,0 +1,21 @@ +--- +'@graphql-mesh/transform-hoist-field': patch +'@graphql-mesh/supergraph': patch +'@graphql-mesh/graphql': patch +'@graphql-mesh/mysql': patch +'@graphql-mesh/neo4j': patch +'@graphql-mesh/fusion-composition': patch +'@graphql-mesh/transport-common': patch +'@graphql-mesh/transport-mysql': patch +'@graphql-mesh/transport-neo4j': patch +'@graphql-mesh/fusion-runtime': patch +'@omnigraph/neo4j': patch +'@graphql-mesh/serve-runtime': patch +'@graphql-mesh/types': patch +'@graphql-mesh/utils': patch +'@graphql-mesh/plugin-hive': patch +'@graphql-mesh/cache-redis': patch +'@graphql-mesh/serve-cli': patch +--- + +Use `Disposable` pattern for plugins and transports diff --git a/.changeset/young-keys-heal.md b/.changeset/young-keys-heal.md new file mode 100644 index 0000000000000..4ad5cde9c8df2 --- /dev/null +++ b/.changeset/young-keys-heal.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/plugin-newrelic': patch +'@graphql-mesh/runtime': patch +'@graphql-mesh/utils': patch +--- + +Simplify the code by using `mapMaybePromise` diff --git a/.github/workflows/loadtest.yml b/.github/workflows/loadtest.yml index 3bb16801830f5..af2b1eacc6c8e 100644 --- a/.github/workflows/loadtest.yml +++ b/.github/workflows/loadtest.yml @@ -2,8 +2,6 @@ name: loadtest on: pull_request: - branches: - - master push: branches: - master diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7e940062cbd14..95d828473d95a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,8 +1,6 @@ name: pr on: pull_request: - branches: - - master jobs: dependencies: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cdfde18f9a182..b7ace14ecbd28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,8 +1,6 @@ name: test on: pull_request: - branches: - - master push: branches: - master diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 8da7689b6538b..bd47d3cdfed40 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -5,8 +5,6 @@ on: branches: - master pull_request: - branches: - - master jobs: deployment: diff --git a/.prettierrc.cjs b/.prettierrc.cjs index 0099c5178b96a..3dff1bfd4e768 100644 --- a/.prettierrc.cjs +++ b/.prettierrc.cjs @@ -2,4 +2,8 @@ const guildConfig = require('@theguild/prettier-config'); module.exports = { ...guildConfig, overrides: [{ files: '*.json', options: { trailingComma: 'none' } }, ...guildConfig.overrides], + importOrderParserPlugins: [ + 'explicitResourceManagement', + ...guildConfig.importOrderParserPlugins, + ] }; diff --git a/babel.config.js b/babel.config.js index d0a267d98c0bd..04700458b918b 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,5 +3,8 @@ module.exports = { ['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }], '@babel/preset-typescript', ], - plugins: ['@babel/plugin-proposal-class-properties'], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-explicit-resource-management', + ], }; diff --git a/declarations.d.ts b/declarations.d.ts index 5460941f19454..24f36d299b387 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -17,3 +17,8 @@ declare module 'newrelic/*' { declare module '@newrelic/test-utilities' { export const TestAgent: any; } + +declare module 'disposablestack/AsyncDisposableStack' { + declare var AsyncDisposableStackCtor: typeof AsyncDisposableStack; + export = AsyncDisposableStackCtor; +} diff --git a/e2e/json-schema-subscriptions/mesh.config.ts b/e2e/json-schema-subscriptions/mesh.config.ts index 54a6ebe0bf4de..e3f47d5595481 100644 --- a/e2e/json-schema-subscriptions/mesh.config.ts +++ b/e2e/json-schema-subscriptions/mesh.config.ts @@ -48,7 +48,6 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg pubsub: new PubSub(), plugins: ctx => [ useWebhooks(ctx), diff --git a/e2e/mysql-rfam/mesh.config.ts b/e2e/mysql-rfam/mesh.config.ts index 2e6c1468f31cc..6a9bcc4f8061b 100644 --- a/e2e/mysql-rfam/mesh.config.ts +++ b/e2e/mysql-rfam/mesh.config.ts @@ -1,6 +1,4 @@ import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; -import { PubSub } from '@graphql-mesh/utils'; import { loadMySQLSubgraph } from '@omnigraph/mysql'; export const composeConfig = defineComposeConfig({ @@ -12,8 +10,3 @@ export const composeConfig = defineComposeConfig({ }, ], }); - -export const serveConfig = defineServeConfig({ - fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg - pubsub: new PubSub(), -}); diff --git a/e2e/neo4j-example/mesh.config.ts b/e2e/neo4j-example/mesh.config.ts index 91fed65718954..f12bfb9426ceb 100644 --- a/e2e/neo4j-example/mesh.config.ts +++ b/e2e/neo4j-example/mesh.config.ts @@ -1,6 +1,4 @@ import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; -import { PubSub } from '@graphql-mesh/utils'; import { loadNeo4JSubgraph } from '@omnigraph/neo4j'; export const composeConfig = defineComposeConfig({ @@ -18,8 +16,3 @@ export const composeConfig = defineComposeConfig({ }, ], }); - -export const serveConfig = defineServeConfig({ - fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg - pubsub: new PubSub(), -}); diff --git a/e2e/openapi-javascript-wiki/mesh.config.ts b/e2e/openapi-javascript-wiki/mesh.config.ts index 56d5c7f368be0..c3fe5bef44373 100644 --- a/e2e/openapi-javascript-wiki/mesh.config.ts +++ b/e2e/openapi-javascript-wiki/mesh.config.ts @@ -21,7 +21,6 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg additionalResolvers: { Query: { async viewsInPastMonth(root, { project }, context: any, info) { diff --git a/e2e/openapi-subscriptions/mesh.config.ts b/e2e/openapi-subscriptions/mesh.config.ts index fae36ae38f2dc..6340473cbc69f 100644 --- a/e2e/openapi-subscriptions/mesh.config.ts +++ b/e2e/openapi-subscriptions/mesh.config.ts @@ -18,7 +18,6 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg pubsub: new PubSub(), plugins: ctx => [useWebhooks(ctx)], }); diff --git a/e2e/pubsub-destroy/CHANGELOG.md b/e2e/pubsub-destroy/CHANGELOG.md deleted file mode 100644 index 121ff92579a77..0000000000000 --- a/e2e/pubsub-destroy/CHANGELOG.md +++ /dev/null @@ -1,101 +0,0 @@ -# @e2e/pubsub-destroy - -## null - -### Patch Changes - -- Updated dependencies []: - - @graphql-mesh/utils@0.98.7 - - @graphql-mesh/serve-cli@0.4.10 - -## null - -### Patch Changes - -- Updated dependencies []: - - @graphql-mesh/serve-cli@0.4.9 - -## null - -### Patch Changes - -- Updated dependencies - [[`270679b`](https://github.com/ardatan/graphql-mesh/commit/270679bb81046727ffe417800cbaa9924fb1bf5c), - [`270679b`](https://github.com/ardatan/graphql-mesh/commit/270679bb81046727ffe417800cbaa9924fb1bf5c)]: - - @graphql-mesh/serve-cli@0.4.8 - - @graphql-mesh/utils@0.98.6 - -## null - -### Patch Changes - -- Updated dependencies - [[`c4d2249`](https://github.com/ardatan/graphql-mesh/commit/c4d22497b4249f9a0969e1d01efbe0721774ce73)]: - - @graphql-mesh/utils@0.98.5 - - @graphql-mesh/serve-cli@0.4.7 - -## null - -### Patch Changes - -- Updated dependencies - [[`fb59244`](https://github.com/ardatan/graphql-mesh/commit/fb592447c12950582881b24c0ca035a34d2ca48c)]: - - @graphql-mesh/utils@0.98.4 - - @graphql-mesh/serve-cli@0.4.6 - -## null - -### Patch Changes - -- Updated dependencies []: - - @graphql-mesh/serve-cli@0.4.5 - -## null - -### Patch Changes - -- Updated dependencies - [[`c47b2aa`](https://github.com/ardatan/graphql-mesh/commit/c47b2aa8c225f04157c1391c638f866bb01edffa)]: - - @graphql-mesh/utils@0.98.3 - - @graphql-mesh/serve-cli@0.4.4 - -## null - -### Patch Changes - -- Updated dependencies []: - - @graphql-mesh/serve-cli@0.4.3 - -## null - -### Patch Changes - -- Updated dependencies - [[`96dd11d`](https://github.com/ardatan/graphql-mesh/commit/96dd11d3c5b70a4971e56d47c8b200d4dc980f38)]: - - @graphql-mesh/utils@0.98.2 - - @graphql-mesh/serve-cli@0.4.2 - -## null - -### Patch Changes - -- Updated dependencies []: - - @graphql-mesh/utils@0.98.1 - - @graphql-mesh/serve-cli@0.4.1 - -## null - -### Patch Changes - -- Updated dependencies - [[`6399add`](https://github.com/ardatan/graphql-mesh/commit/6399addeeca2d5cf0bf545c537d01c784de65e84), - [`31828ad`](https://github.com/ardatan/graphql-mesh/commit/31828ad87a0c4d616f1217282bd1e7e74324fd9c), - [`2fcadce`](https://github.com/ardatan/graphql-mesh/commit/2fcadce67b9acbcab2a14aa9ea57dbb84101f0b5), - [`6399add`](https://github.com/ardatan/graphql-mesh/commit/6399addeeca2d5cf0bf545c537d01c784de65e84), - [`6399add`](https://github.com/ardatan/graphql-mesh/commit/6399addeeca2d5cf0bf545c537d01c784de65e84), - [`6399add`](https://github.com/ardatan/graphql-mesh/commit/6399addeeca2d5cf0bf545c537d01c784de65e84), - [`31828ad`](https://github.com/ardatan/graphql-mesh/commit/31828ad87a0c4d616f1217282bd1e7e74324fd9c), - [`31828ad`](https://github.com/ardatan/graphql-mesh/commit/31828ad87a0c4d616f1217282bd1e7e74324fd9c), - [`6399add`](https://github.com/ardatan/graphql-mesh/commit/6399addeeca2d5cf0bf545c537d01c784de65e84)]: - - @graphql-mesh/serve-cli@0.4.0 - - @graphql-mesh/utils@0.98.0 diff --git a/e2e/pubsub-destroy/mesh.config.ts b/e2e/pubsub-destroy/mesh.config.ts deleted file mode 100644 index c2084cd25a18a..0000000000000 --- a/e2e/pubsub-destroy/mesh.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createServer } from 'http'; -import { defineConfig } from '@graphql-mesh/serve-cli'; -import { PubSub } from '@graphql-mesh/utils'; - -const pubsub = new PubSub(); - -// start a server that doesnt close until the pubsub is destroyed -// if the pubsub doesnt get destroyed, the process will hang and not die -const server = createServer(); -server.listen(); -pubsub.subscribe('destroy', () => server.close()); - -export const serveConfig = defineConfig({ - fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg - pubsub, -}); diff --git a/e2e/pubsub-destroy/package.json b/e2e/pubsub-destroy/package.json deleted file mode 100644 index fee6a0dfac6e8..0000000000000 --- a/e2e/pubsub-destroy/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@e2e/pubsub-destroy", - "private": true, - "dependencies": { - "@graphql-mesh/serve-cli": "workspace:*", - "@graphql-mesh/utils": "workspace:*" - } -} diff --git a/e2e/pubsub-destroy/pubsub-destroy.test.ts b/e2e/pubsub-destroy/pubsub-destroy.test.ts deleted file mode 100644 index 8c6ba53617025..0000000000000 --- a/e2e/pubsub-destroy/pubsub-destroy.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createTenv } from '@e2e/tenv'; - -const { serve } = createTenv(__dirname); - -it('should destroy pubsub on process kill signal', async () => { - const { dispose } = await serve(); - await expect(dispose()).resolves.toBeUndefined(); -}); diff --git a/examples/grpc-example/tests/grpc.test.ts b/examples/grpc-example/tests/grpc.test.ts index 6256599d20d81..714ccbce8b273 100644 --- a/examples/grpc-example/tests/grpc.test.ts +++ b/examples/grpc-example/tests/grpc.test.ts @@ -1,4 +1,3 @@ -import 'json-bigint-patch'; import { join } from 'path'; import { readFile } from 'fs-extra'; import { findAndParseConfig } from '@graphql-mesh/cli'; diff --git a/jest.config.js b/jest.config.js index edd42072924c9..f48f7d70062d0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -42,7 +42,7 @@ if (process.env.E2E_TEST) { } else { testMatch.push('!**/e2e/**/?(*.)+(spec|test).[jt]s?(x)'); } - +/** @type {import('jest').Config} */ module.exports = { prettierPath: null, // not supported before Jest v30 https://github.com/jestjs/jest/issues/14305 testEnvironment: 'node', @@ -65,4 +65,5 @@ module.exports = { }, resolver: 'bob-the-bundler/jest-resolver', testMatch, + setupFilesAfterEnv: ['/setup-jest.js'], }; diff --git a/package.json b/package.json index 3453e47623709..ffb777a747bda 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@ardatan/graphql-to-config-schema": "0.1.25", "@babel/core": "7.24.7", "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/plugin-proposal-explicit-resource-management": "7.24.7", "@babel/preset-env": "7.24.7", "@babel/preset-typescript": "7.24.7", "@changesets/changelog-github": "0.5.0", diff --git a/packages/cache/redis/__integration_tests__/redis.spec.ts b/packages/cache/redis/__integration_tests__/redis.spec.ts index acab10e3e1611..42693a0f2282f 100644 --- a/packages/cache/redis/__integration_tests__/redis.spec.ts +++ b/packages/cache/redis/__integration_tests__/redis.spec.ts @@ -1,17 +1,14 @@ import RedisCache from '@graphql-mesh/cache-redis'; -import { DefaultLogger, PubSub } from '@graphql-mesh/utils'; +import { DefaultLogger } from '@graphql-mesh/utils'; describe('Redis', () => { - const pubsub = new PubSub(); const logger = new DefaultLogger('test'); - const redisCache = new RedisCache({ - host: '{env.REDIS_HOST}', - port: '{env.REDIS_PORT}', - pubsub, - logger, - }); - afterAll(() => pubsub.publish('destroy', undefined)); it('works', async () => { + using redisCache = new RedisCache({ + host: '{env.REDIS_HOST}', + port: '{env.REDIS_PORT}', + logger, + }); const test = await redisCache.get('test'); expect(test).toBeUndefined(); const now = Date.now(); diff --git a/packages/cache/redis/src/index.ts b/packages/cache/redis/src/index.ts index 8e00ae4429a58..dae6b70321f10 100644 --- a/packages/cache/redis/src/index.ts +++ b/packages/cache/redis/src/index.ts @@ -14,10 +14,10 @@ function interpolateStrWithEnv(str: string): string { return stringInterpolator.parse(str, { env: process.env }); } -export default class RedisCache implements KeyValueCache { +export default class RedisCache implements KeyValueCache, Disposable { private client: Redis; - constructor(options: YamlConfig.Cache['redis'] & { pubsub: MeshPubSub; logger: Logger }) { + constructor(options: YamlConfig.Cache['redis'] & { pubsub?: MeshPubSub; logger: Logger }) { const lazyConnect = options.lazyConnect !== false; if (options.url) { @@ -55,12 +55,17 @@ export default class RedisCache implements KeyValueCache { this.client = new RedisMock(); } } - const id = options.pubsub.subscribe('destroy', () => { + // TODO: PubSub.destroy will no longer be needed after v0 + const id = options.pubsub?.subscribe('destroy', () => { this.client.disconnect(false); options.pubsub.unsubscribe(id); }); } + [Symbol.dispose](): void { + this.client.disconnect(); + } + async set(key: string, value: V, options?: KeyValueCacheSetOptions): Promise { const stringifiedValue = JSON.stringify(value); if (options?.ttl) { diff --git a/packages/cache/redis/test/cache.spec.ts b/packages/cache/redis/test/cache.spec.ts index 33f2285d13fbf..5e74aa42cd217 100644 --- a/packages/cache/redis/test/cache.spec.ts +++ b/packages/cache/redis/test/cache.spec.ts @@ -1,24 +1,23 @@ /* eslint-disable no-new */ import Redis from 'ioredis'; -import { DefaultLogger, PubSub } from '@graphql-mesh/utils'; +import { DefaultLogger } from '@graphql-mesh/utils'; import RedisCache from '../src/index.js'; jest.mock('ioredis'); describe('redis', () => { beforeEach(() => jest.clearAllMocks()); - const pubsub = new PubSub(); const logger = new DefaultLogger('test'); describe('constructor', () => { it('never call Redis constructor if no config is provided', async () => { - new RedisCache({ pubsub, logger }); + using redis = new RedisCache({ logger }); expect(Redis).toHaveBeenCalledTimes(0); }); it('passes configuration to redis client with default options, url case', async () => { - new RedisCache({ url: 'redis://password@localhost:6379', pubsub, logger }); + using redis = new RedisCache({ url: 'redis://password@localhost:6379', logger }); expect(Redis).toHaveBeenCalledWith( 'redis://password@localhost:6379?lazyConnect=true&enableAutoPipelining=true&enableOfflineQueue=true', @@ -26,10 +25,9 @@ describe('redis', () => { }); it('passes configuration to redis client with default options, url and lazyConnect (=false) case', async () => { - new RedisCache({ + using redis = new RedisCache({ url: 'redis://password@localhost:6379', lazyConnect: false, - pubsub, logger, }); @@ -39,10 +37,9 @@ describe('redis', () => { }); it('passes configuration to redis client with default options, url and lazyConnect (=true) case', async () => { - new RedisCache({ + using redis = new RedisCache({ url: 'redis://password@localhost:6379', lazyConnect: true, - pubsub, logger, }); @@ -52,7 +49,12 @@ describe('redis', () => { }); it('passes configuration to redis client with default options, host, port & password case', async () => { - new RedisCache({ port: '6379', password: 'testpassword', host: 'localhost', pubsub, logger }); + using redis = new RedisCache({ + port: '6379', + password: 'testpassword', + host: 'localhost', + logger, + }); expect(Redis).toHaveBeenCalledWith({ enableAutoPipelining: true, @@ -65,12 +67,11 @@ describe('redis', () => { }); it('passes configuration to redis client with default options, host, port, password & lazyConnect (=false) case', async () => { - new RedisCache({ + using redis = new RedisCache({ port: '6379', password: 'testpassword', host: 'localhost', lazyConnect: false, - pubsub, logger, }); @@ -84,11 +85,10 @@ describe('redis', () => { }); it('prefers url over specific properties if both given', () => { - new RedisCache({ + using redis = new RedisCache({ url: 'redis://localhost:6379', host: 'ignoreme', port: '9999', - pubsub, logger, }); @@ -101,7 +101,7 @@ describe('redis', () => { 'throws an error if protocol does not match [%s]', protocol => { expect(() => { - new RedisCache({ url: `${protocol}localhost:6379`, pubsub, logger }); + using redis = new RedisCache({ url: `${protocol}localhost:6379`, logger }); }).toThrowError('Redis URL must use either redis:// or rediss://'); }, ); @@ -121,9 +121,8 @@ describe('redis', () => { delete process.env.REDIS_PASSWORD; }); it('supports string interpolation for url', async () => { - new RedisCache({ + using redis = new RedisCache({ url: '{env.REDIS_URL}', - pubsub, logger, }); expect(Redis).toHaveBeenCalledWith( @@ -131,11 +130,10 @@ describe('redis', () => { ); }); it('supports string interpolation for host, port and password', async () => { - new RedisCache({ + using redis = new RedisCache({ host: '{env.REDIS_HOST}', port: '{env.REDIS_PORT}', password: '{env.REDIS_PASSWORD}', - pubsub, logger, }); expect(Redis).toHaveBeenCalledWith({ diff --git a/packages/compose-cli/package.json b/packages/compose-cli/package.json index 08558963cb36b..902c975bf1ed4 100644 --- a/packages/compose-cli/package.json +++ b/packages/compose-cli/package.json @@ -12,7 +12,7 @@ "node": ">=16.0.0" }, "bin": { - "mesh-compose": "dist/cjs/bin.js" + "mesh-compose": "dist/esm/bin.js" }, "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/compose-cli/src/run.ts b/packages/compose-cli/src/run.ts index 03e33a5e71d1b..6190445f9818f 100644 --- a/packages/compose-cli/src/run.ts +++ b/packages/compose-cli/src/run.ts @@ -1,4 +1,5 @@ -import 'tsx/cjs'; // support importing typescript configs +import 'tsx/cjs'; // support importing typescript configs in CommonJS +import 'tsx/esm'; // support importing typescript configs in ESM import 'dotenv/config'; // inject dotenv options to process.env // eslint-disable-next-line import/no-nodejs-modules @@ -34,6 +35,8 @@ export interface RunOptions extends ReturnType { version?: string; } +export type ImportedModule = T | { default: T }; + export async function run({ log: rootLog = new DefaultLogger(), productName = 'Mesh Compose', @@ -52,23 +55,27 @@ export async function run({ ? opts.configPath : resolve(process.cwd(), opts.configPath); log.info(`Checking configuration at ${configPath}`); - const importedConfig: { composeConfig?: MeshComposeCLIConfig } = await import(configPath).catch( - err => { + const importedConfigModule: ImportedModule<{ composeConfig?: MeshComposeCLIConfig }> = + await import(configPath).catch(err => { if (err.code === 'ERR_MODULE_NOT_FOUND') { return {}; // no config is ok } log.error('Loading configuration failed!'); throw err; - }, - ); - if (importedConfig.composeConfig) { - log.info('Loaded configuration'); + }); + + let importedConfig: MeshComposeCLIConfig; + if ('default' in importedConfigModule) { + importedConfig = importedConfigModule.default.composeConfig; + } else if ('composeConfig' in importedConfigModule) { + importedConfig = importedConfigModule.composeConfig; } else { throw new Error(`No configuration found at ${configPath}`); } + log.info('Loaded configuration'); const config: MeshComposeCLIConfig = { - ...importedConfig?.composeConfig, + ...importedConfig, ...opts, }; diff --git a/packages/fusion/runtime/package.json b/packages/fusion/runtime/package.json index c91fd1d0d5842..9273c0a5fb9dc 100644 --- a/packages/fusion/runtime/package.json +++ b/packages/fusion/runtime/package.json @@ -57,10 +57,12 @@ "@graphql-mesh/utils": "^0.98.7", "@graphql-tools/delegate": "^10.0.11", "@graphql-tools/executor": "^1.2.6", + "@graphql-tools/federation": "^2.0.0", "@graphql-tools/stitch": "^9.2.9", "@graphql-tools/stitching-directives": "^3.0.2", "@graphql-tools/utils": "^10.2.1", "@graphql-tools/wrap": "^10.0.5", + "disposablestack": "^1.1.6", "graphql-yoga": "^5.3.0", "tslib": "^2.4.0" }, diff --git a/packages/fusion/runtime/src/federation.ts b/packages/fusion/runtime/src/federation.ts new file mode 100644 index 0000000000000..947a68811c8a9 --- /dev/null +++ b/packages/fusion/runtime/src/federation.ts @@ -0,0 +1,55 @@ +import { DocumentNode, GraphQLSchema, isSchema, parse } from 'graphql'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { TransportEntry } from '@graphql-mesh/transport-common'; +import { SubschemaConfig } from '@graphql-tools/delegate'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { getStitchedSchemaFromSupergraphSdl } from '@graphql-tools/federation'; +import { getDocumentNodeFromSchema } from '@graphql-tools/utils'; +import { UnifiedGraphHandler } from './unifiedGraphManager.js'; + +function ensureSchemaAST(source: GraphQLSchema | DocumentNode | string) { + if (isSchema(source)) { + return getDocumentNodeFromSchema(source); + } + if (typeof source === 'string') { + return parse(source, { noLocation: true }); + } + return source; +} + +export const handleFederationSupergraph: UnifiedGraphHandler = function ({ + unifiedGraph, + onSubgraphExecute, + additionalTypeDefs, + additionalResolvers = [], +}) { + const supergraphSdl = ensureSchemaAST(unifiedGraph); + const transportEntryMap: Record = {}; + let subschemas: SubschemaConfig[] = []; + const newUnifiedGraph = getStitchedSchemaFromSupergraphSdl({ + supergraphSdl, + onSubschemaConfig(subschemaConfig) { + const subschemaName = subschemaConfig.name; + transportEntryMap[subschemaName] = { + kind: 'http', + subgraph: subschemaName, + location: subschemaConfig.endpoint, + }; + subschemaConfig.executor = function subschemaExecutor(req) { + return onSubgraphExecute(subschemaName, req); + }; + }, + batch: true, + onStitchingOptions(opts: any) { + subschemas = opts.subschemas; + opts.typeDefs = [opts.typeDefs, additionalTypeDefs]; + opts.resolvers = additionalResolvers; + }, + }); + return { + unifiedGraph: newUnifiedGraph, + subschemas, + transportEntryMap, + additionalResolvers: [], + }; +}; diff --git a/packages/fusion/runtime/src/getSubschemasFromFusiongraph.ts b/packages/fusion/runtime/src/getSubschemasFromFusiongraph.ts index 69d3e19928b82..46b7ad0b1d7a0 100644 --- a/packages/fusion/runtime/src/getSubschemasFromFusiongraph.ts +++ b/packages/fusion/runtime/src/getSubschemasFromFusiongraph.ts @@ -1,34 +1,23 @@ import { - ASTNode, - buildASTSchema, - ConstDirectiveNode, - DocumentNode, - getArgumentValues, GraphQLArgument, GraphQLFieldConfigArgumentMap, GraphQLSchema, isOutputType, isSpecifiedScalarType, - Kind, parseType, - print, - printSchema, printType, typeFromAST, - valueFromASTUntyped, visit, } from 'graphql'; import { TransportEntry } from '@graphql-mesh/transport-common'; -import { getDefDirectives, resolveAdditionalResolvers } from '@graphql-mesh/utils'; +import { + getDefDirectives, + resolveAdditionalResolvers, + resolveAdditionalResolversWithoutImport, +} from '@graphql-mesh/utils'; import { SubschemaConfig, Transform } from '@graphql-tools/delegate'; import { stitchingDirectives } from '@graphql-tools/stitching-directives'; -import { - DirectiveAnnotation, - getRootTypeNames, - MapperKind, - mapSchema, - printSchemaWithDirectives, -} from '@graphql-tools/utils'; +import { getRootTypeNames, IResolvers, MapperKind, mapSchema } from '@graphql-tools/utils'; import { HoistField, RenameInputObjectFields, @@ -39,9 +28,12 @@ import { TransformEnumValues, } from '@graphql-tools/wrap'; -export function extractSubgraphsFromFusiongraph(fusiongraph: GraphQLSchema) { +export function extractSubgraphsFromFusiongraph( + fusiongraph: GraphQLSchema, + onSubschemaConfig?: (subschema: SubschemaConfig) => void, +) { const subgraphNames = new Set(); - const subschemaMap = new Map(); + const subschemas: SubschemaConfig[] = []; const transportEntryMap: Record = {}; const schemaDirectives = getDefDirectives(fusiongraph, fusiongraph); const transportDirectives = schemaDirectives.filter(directive => directive.name === 'transport'); @@ -54,10 +46,7 @@ export function extractSubgraphsFromFusiongraph(fusiongraph: GraphQLSchema) { } } const rootTypeNames = getRootTypeNames(fusiongraph); - const additionalResolversFromTypeDefs: Exclude< - Parameters[1][0], - string - >[] = []; + const additionalResolvers: IResolvers[] = []; const additionalTypeDefs = new Set(); for (const subgraph of subgraphNames) { const renameTypeNames: Record = {}; @@ -102,11 +91,13 @@ export function extractSubgraphsFromFusiongraph(fusiongraph: GraphQLSchema) { ); if (resolveToDirectives.length > 0) { for (const resolveToDirective of resolveToDirectives) { - additionalResolversFromTypeDefs.push({ - targetTypeName: type.name, - targetFieldName: fieldName, - ...(resolveToDirective.args as any), - }); + additionalResolvers.push( + resolveAdditionalResolversWithoutImport({ + targetTypeName: type.name, + targetFieldName: fieldName, + ...(resolveToDirective.args as any), + }), + ); } } } @@ -120,11 +111,13 @@ export function extractSubgraphsFromFusiongraph(fusiongraph: GraphQLSchema) { ); if (resolveToDirectives.length > 0) { for (const resolveToDirective of resolveToDirectives) { - additionalResolversFromTypeDefs.push({ - targetTypeName: typeName, - targetFieldName: fieldName, - ...(resolveToDirective.args as any), - }); + additionalResolvers.push( + resolveAdditionalResolversWithoutImport({ + targetTypeName: typeName, + targetFieldName: fieldName, + ...(resolveToDirective.args as any), + }), + ); } } const sourceDirectives = fieldDirectives.filter(directive => directive.name === 'source'); @@ -357,6 +350,7 @@ export function extractSubgraphsFromFusiongraph(fusiongraph: GraphQLSchema) { ); } let subschema: SubschemaConfig = { + name: subgraph, schema: subgraphSchema, transforms, }; @@ -385,13 +379,14 @@ export function extractSubgraphsFromFusiongraph(fusiongraph: GraphQLSchema) { } subschema.merge = mergeConfig; } - subschemaMap.set(subgraph, subschema); + onSubschemaConfig?.(subschema); + subschemas.push(subschema); } return { - subschemaMap, + subschemas, transportEntryMap, additionalTypeDefs, - additionalResolversFromTypeDefs, + additionalResolvers, }; } diff --git a/packages/fusion/runtime/src/index.ts b/packages/fusion/runtime/src/index.ts index 4ecf6b04c6180..791f49e271224 100644 --- a/packages/fusion/runtime/src/index.ts +++ b/packages/fusion/runtime/src/index.ts @@ -1,3 +1,4 @@ export * from './utils.js'; -export * from './useFusiongraph.js'; +export * from './unifiedGraphManager.js'; export * from './getSubschemasFromFusiongraph.js'; +export * from './federation.js'; diff --git a/packages/fusion/runtime/src/unifiedGraphManager.ts b/packages/fusion/runtime/src/unifiedGraphManager.ts new file mode 100644 index 0000000000000..c5d82ae12fc72 --- /dev/null +++ b/packages/fusion/runtime/src/unifiedGraphManager.ts @@ -0,0 +1,234 @@ +import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; +import { buildASTSchema, buildSchema, DocumentNode, GraphQLSchema, isSchema } from 'graphql'; +import { getInContextSDK } from '@graphql-mesh/runtime'; +import { TransportBaseContext, TransportEntry } from '@graphql-mesh/transport-common'; +import { OnDelegateHook } from '@graphql-mesh/types'; +import { mapMaybePromise } from '@graphql-mesh/utils'; +import { SubschemaConfig } from '@graphql-tools/delegate'; +import { stitchSchemas } from '@graphql-tools/stitch'; +import { + IResolvers, + isDocumentNode, + MaybePromise, + pruneSchema, + TypeSource, +} from '@graphql-tools/utils'; +import { filterHiddenPartsInSchema } from './filterHiddenPartsInSchema.js'; +import { extractSubgraphsFromFusiongraph } from './getSubschemasFromFusiongraph.js'; +import { + compareSchemas, + getOnSubgraphExecute, + OnSubgraphExecuteHook, + TransportsOption, +} from './utils.js'; + +function ensureSchema(source: GraphQLSchema | DocumentNode | string) { + if (isSchema(source)) { + return source; + } + if (typeof source === 'string') { + return buildSchema(source, { assumeValid: true, assumeValidSDL: true }); + } + if (isDocumentNode(source)) { + return buildASTSchema(source, { assumeValid: true, assumeValidSDL: true }); + } + return source; +} + +export interface GetExecutableSchemaFromFusiongraphOptions> { + additionalTypeDefs?: DocumentNode | string | DocumentNode[] | string[]; + additionalResolvers?: IResolvers | IResolvers[]; + transportBaseContext?: TransportBaseContext; +} + +export type UnifiedGraphHandler = (opts: UnifiedGraphHandlerOpts) => UnifiedGraphHandlerResult; + +export interface UnifiedGraphHandlerOpts { + unifiedGraph: GraphQLSchema; + additionalTypeDefs?: TypeSource; + additionalResolvers?: IResolvers | IResolvers[]; + onSubgraphExecute: ReturnType; +} + +export interface UnifiedGraphHandlerResult { + unifiedGraph: GraphQLSchema; + transportEntryMap: Record; + subschemas: SubschemaConfig[]; + additionalResolvers: IResolvers[]; +} + +export interface UnifiedGraphManagerOptions { + getUnifiedGraph( + baseCtx: TransportBaseContext, + ): MaybePromise; + // Handle the unified graph by any specification + handleUnifiedGraph?: UnifiedGraphHandler; + transports?: TransportsOption; + polling?: number; + additionalTypeDefs?: TypeSource; + additionalResolvers?: IResolvers | IResolvers[]; + transportBaseContext?: TransportBaseContext; + readinessCheckEndpoint?: string; + onSubgraphExecuteHooks?: OnSubgraphExecuteHook[]; + // TODO: Will be removed later once we get rid of v0 + onDelegateHooks?: OnDelegateHook[]; +} + +export const handleFusiongraph: UnifiedGraphHandler = function handleFusiongraph(opts) { + const { subschemas, transportEntryMap, additionalTypeDefs, additionalResolvers } = + extractSubgraphsFromFusiongraph(opts.unifiedGraph, function (subschemaConfig) { + subschemaConfig.executor = function executor(execReq) { + return opts.onSubgraphExecute(subschemaConfig.name, execReq); + }; + }); + const unifiedGraph = pruneSchema( + filterHiddenPartsInSchema( + stitchSchemas({ + subschemas, + assumeValid: true, + assumeValidSDL: true, + typeDefs: [opts.additionalTypeDefs, ...additionalTypeDefs], + resolvers: [opts.additionalResolvers as any, ...additionalResolvers], + }), + ), + ); + + return { + unifiedGraph, + transportEntryMap, + subschemas, + additionalTypeDefs, + additionalResolvers, + }; +}; + +export class UnifiedGraphManager { + private handleUnifiedGraph: UnifiedGraphHandler; + private unifiedGraph: GraphQLSchema; + private lastLoadedUnifiedGraph: string | GraphQLSchema | DocumentNode; + private onSubgraphExecuteHooks: OnSubgraphExecuteHook[]; + private currentTimeout: NodeJS.Timeout | undefined; + private inContextSDK; + private initialUnifiedGraph$: MaybePromise; + private disposableStack = new AsyncDisposableStack(); + constructor(private opts: UnifiedGraphManagerOptions) { + this.handleUnifiedGraph = opts.handleUnifiedGraph || handleFusiongraph; + this.onSubgraphExecuteHooks = opts?.onSubgraphExecuteHooks || []; + this.disposableStack.defer(() => { + this.unifiedGraph = undefined; + this.lastLoadedUnifiedGraph = undefined; + this.inContextSDK = undefined; + this.initialUnifiedGraph$ = undefined; + this.pausePolling(); + }); + } + + private pausePolling() { + if (this.currentTimeout) { + clearTimeout(this.currentTimeout); + this.currentTimeout = undefined; + } + } + + private continuePolling() { + if (this.opts.polling) { + this.currentTimeout = setTimeout(() => { + this.currentTimeout = undefined; + return this.getAndSetUnifiedGraph(); + }, this.opts.polling); + } + } + + private ensureUnifiedGraph() { + if (!this.initialUnifiedGraph$) { + this.initialUnifiedGraph$ = this.getAndSetUnifiedGraph(); + } + return this.initialUnifiedGraph$; + } + + private getAndSetUnifiedGraph() { + this.pausePolling(); + return mapMaybePromise( + this.opts.getUnifiedGraph(this.opts.transportBaseContext), + (loadedUnifiedGraph: string | GraphQLSchema | DocumentNode) => { + if ( + loadedUnifiedGraph != null && + this.lastLoadedUnifiedGraph != null && + compareSchemas(loadedUnifiedGraph, this.lastLoadedUnifiedGraph) + ) { + this.opts.transportBaseContext?.logger?.debug( + 'Unified Graph has not changed, skipping...', + ); + this.continuePolling(); + return; + } + if (this.lastLoadedUnifiedGraph != null) { + this.opts.transportBaseContext?.logger?.debug('Unified Graph changed, updating...'); + } + this.lastLoadedUnifiedGraph ||= loadedUnifiedGraph; + this.lastLoadedUnifiedGraph = loadedUnifiedGraph; + this.unifiedGraph = ensureSchema(loadedUnifiedGraph); + const { + unifiedGraph: newUnifiedGraph, + transportEntryMap, + subschemas, + additionalResolvers, + } = this.handleUnifiedGraph({ + unifiedGraph: this.unifiedGraph, + additionalTypeDefs: this.opts.additionalTypeDefs, + additionalResolvers: this.opts.additionalResolvers, + onSubgraphExecute(subgraphName, execReq) { + return onSubgraphExecute(subgraphName, execReq); + }, + }); + this.unifiedGraph = newUnifiedGraph; + const onSubgraphExecute = getOnSubgraphExecute({ + onSubgraphExecuteHooks: this.onSubgraphExecuteHooks, + transports: this.opts.transports, + transportBaseContext: this.opts.transportBaseContext, + transportEntryMap, + getSubgraphSchema(subgraphName) { + const subgraph = subschemas.find(s => s.name === subgraphName); + if (!subgraph) { + throw new Error(`Subgraph ${subgraphName} not found`); + } + return subgraph.schema; + }, + disposableStack: this.disposableStack, + }); + if (this.opts.additionalResolvers || additionalResolvers.length) { + this.inContextSDK = getInContextSDK( + this.unifiedGraph, + // @ts-expect-error Legacy Mesh RawSource is not compatible with new Mesh + subschemas, + this.opts.transportBaseContext?.logger, + this.opts.onDelegateHooks || [], + ); + } + this.continuePolling(); + }, + ); + } + + public getUnifiedGraph() { + return mapMaybePromise(this.ensureUnifiedGraph(), () => this.unifiedGraph); + } + + public getContext(base: T = {} as T) { + return mapMaybePromise(this.ensureUnifiedGraph(), () => { + if (this.inContextSDK) { + Object.assign(base, this.inContextSDK); + } + Object.assign(base, this.opts.transportBaseContext); + return base; + }); + } + + invalidateUnifiedGraph() { + return this.getAndSetUnifiedGraph(); + } + + [Symbol.asyncDispose]() { + return this.disposableStack.disposeAsync(); + } +} diff --git a/packages/fusion/runtime/src/useFusiongraph.ts b/packages/fusion/runtime/src/useFusiongraph.ts deleted file mode 100644 index 0e5890fac14bb..0000000000000 --- a/packages/fusion/runtime/src/useFusiongraph.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { - buildASTSchema, - buildSchema, - DocumentNode, - GraphQLSchema, - isSchema, - specifiedRules, - validate, -} from 'graphql'; -import { envelop, PromiseOrValue, useReadinessCheck, type Plugin } from 'graphql-yoga'; -import { useEngine } from '@envelop/core'; -import { getInContextSDK } from '@graphql-mesh/runtime'; -import { TransportBaseContext } from '@graphql-mesh/transport-common'; -import { OnDelegateHook } from '@graphql-mesh/types'; -import { - mapMaybePromise, - parseWithCache, - resolveAdditionalResolversWithoutImport, -} from '@graphql-mesh/utils'; -import { SubschemaConfig } from '@graphql-tools/delegate'; -import { normalizedExecutor } from '@graphql-tools/executor'; -import { stitchSchemas } from '@graphql-tools/stitch'; -import { - ExecutionResult, - IResolvers, - isAsyncIterable, - isDocumentNode, - isPromise, - mapAsyncIterator, - mapSchema, - MaybeAsyncIterable, - MaybePromise, - pruneSchema, -} from '@graphql-tools/utils'; -import { TypedDocumentNode } from '@graphql-typed-document-node/core'; -import { filterHiddenPartsInSchema } from './filterHiddenPartsInSchema.js'; -import { extractSubgraphsFromFusiongraph } from './getSubschemasFromFusiongraph.js'; -import { - defaultTransportsOption, - FusiongraphPlugin, - getOnSubgraphExecute, - TransportsOption, -} from './utils.js'; - -function ensureSchema(source: GraphQLSchema | DocumentNode | string) { - if (isSchema(source)) { - return source; - } - if (typeof source === 'string') { - return buildSchema(source, { assumeValid: true, assumeValidSDL: true }); - } - if (isDocumentNode(source)) { - return buildASTSchema(source, { assumeValid: true, assumeValidSDL: true }); - } - return source; -} - -export interface GetExecutableSchemaFromFusiongraphOptions> { - additionalTypedefs?: DocumentNode | string | DocumentNode[] | string[]; - additionalResolvers?: IResolvers | IResolvers[]; - transportBaseContext?: TransportBaseContext; -} - -export interface FusiongraphPluginOptions { - getFusiongraph( - baseCtx: TransportBaseContext, - ): GraphQLSchema | DocumentNode | string | Promise; - transports?: TransportsOption; - polling?: number; - additionalTypedefs?: DocumentNode | string | DocumentNode[] | string[]; - additionalResolvers?: IResolvers | IResolvers[]; - transportBaseContext?: TransportBaseContext; - readinessCheckEndpoint?: string; -} - -export type EnvelopFusiongraphOpts = FusiongraphPluginOptions & { - plugins?: (Plugin & FusiongraphPlugin)[]; -}; - -export function envelopFusiongraph = Record>( - opts: EnvelopFusiongraphOpts, -) { - return envelop({ - plugins: [ - useEngine({ - execute: normalizedExecutor, - validate, - parse: parseWithCache, - specifiedRules, - }), - useFusiongraph(opts), - ...(opts.plugins || []), - ], - }); -} - -export function getExecutorForFusiongraph>( - opts: EnvelopFusiongraphOpts, -) { - const getEnveloped = envelopFusiongraph(opts); - return function fusiongraphExecutor(executorOpts: { - query: TypedDocumentNode | string; - variables?: TVariables; - context?: unknown; - }): MaybePromise> { - const { parse, validate, contextFactory, execute, schema } = getEnveloped(executorOpts.context); - const document = - typeof executorOpts.query === 'string' ? parse(executorOpts.query) : executorOpts.query; - - if (schema) { - const validationErrors = validate(schema, document); - if (validationErrors.length) { - if (validationErrors.length === 1) { - throw validationErrors[0]; - } else { - throw new AggregateError( - validationErrors, - validationErrors.map(err => err.message).join('\n'), - ); - } - } - } - - // @ts-expect-error Somehow contextFactory typings are not correct - return mapMaybePromise(contextFactory(), context => { - const executionResult$ = execute({ - document, - schema, - variableValues: executorOpts.variables, - contextValue: context, - }); - return mapMaybePromise( - executionResult$, - (executionResult: MaybeAsyncIterable) => { - function handleSingleResult(result: ExecutionResult) { - if (result.errors) { - if (result.errors.length === 1) { - throw result.errors[0]; - } - throw new AggregateError(result.errors, 'Multiple errors occurred'); - } - return result.data; - } - if (isAsyncIterable(executionResult)) { - const iterator = executionResult[Symbol.asyncIterator](); - return mapAsyncIterator(iterator, handleSingleResult); - } - return handleSingleResult(executionResult); - }, - ); - }); - }; -} - -export function useFusiongraph = Record>( - opts: FusiongraphPluginOptions, -): Plugin & { - invalidateUnifiedGraph(): void; -} { - let fusiongraph: GraphQLSchema; - let lastLoadedFusiongraph: string | GraphQLSchema | DocumentNode; - let plugins: (Plugin & FusiongraphPlugin)[]; - // TODO: We need to figure this out in a better way - let inContextSDK: any; - function handleLoadedFusiongraph(loadedFusiongraph: string | GraphQLSchema | DocumentNode) { - if (loadedFusiongraph != null && lastLoadedFusiongraph === loadedFusiongraph) { - return; - } - lastLoadedFusiongraph = loadedFusiongraph; - fusiongraph = ensureSchema(loadedFusiongraph); - const { transportEntryMap, subschemaMap, additionalTypeDefs, additionalResolversFromTypeDefs } = - extractSubgraphsFromFusiongraph(fusiongraph); - const subgraphMap = new Map(); - const subschemas: SubschemaConfig[] = []; - const onSubgraphExecute = getOnSubgraphExecute({ - fusiongraph, - plugins, - transports: opts.transports || defaultTransportsOption, - transportBaseContext: opts.transportBaseContext, - transportEntryMap, - subgraphMap, - }); - for (const [subschemaName, subschemaConfig] of subschemaMap) { - subgraphMap.set(subschemaName, subschemaConfig.schema); - subschemas.push({ - ...subschemaConfig, - name: subschemaName, - executor(execReq) { - return onSubgraphExecute(subschemaName, execReq); - }, - } as SubschemaConfig); - } - fusiongraph = stitchSchemas({ - subschemas, - assumeValid: true, - assumeValidSDL: true, - typeDefs: [opts.additionalTypedefs, ...additionalTypeDefs], - resolvers: [ - opts.additionalResolvers as any, - additionalResolversFromTypeDefs.map(additionalResolver => - resolveAdditionalResolversWithoutImport(additionalResolver), - ), - ] as any, - }); - fusiongraph = filterHiddenPartsInSchema(fusiongraph); - fusiongraph = pruneSchema(fusiongraph); - if (opts.additionalResolvers || additionalResolversFromTypeDefs.length) { - const onDelegateHooks: OnDelegateHook[] = []; - for (const plugin of plugins as any[]) { - if (plugin.onDelegate) { - onDelegateHooks.push(plugin.onDelegate); - } - } - inContextSDK = getInContextSDK( - fusiongraph, - subschemas as any[], - opts.transportBaseContext?.logger, - onDelegateHooks, - ); - } - } - function getAndSetFusiongraph(): PromiseOrValue { - const supergraph$ = opts.getFusiongraph(opts.transportBaseContext); - return mapMaybePromise(supergraph$, handleLoadedFusiongraph) as PromiseOrValue; - } - if (opts.polling) { - setInterval(getAndSetFusiongraph, opts.polling); - } - let initialFusiongraph$: PromiseOrValue; - let initiated = false; - function ensureFusiongraph() { - if (!initiated) { - initialFusiongraph$ = getAndSetFusiongraph(); - } - initiated = true; - return initialFusiongraph$; - } - return { - onPluginInit({ addPlugin, plugins: allPlugins }) { - plugins = allPlugins as any; - if (opts.readinessCheckEndpoint) { - addPlugin( - // TODO: fix useReadinessCheck typings to inherit the context - useReadinessCheck({ - endpoint: opts.readinessCheckEndpoint, - check() { - const initialFusiongraph$ = ensureFusiongraph(); - if (isPromise(initialFusiongraph$)) { - return initialFusiongraph$.then(() => !!fusiongraph); - } - return !!fusiongraph; - }, - }) as any, - ); - } - }, - onRequestParse() { - return { - onRequestParseDone() { - return ensureFusiongraph(); - }, - }; - }, - onEnveloped({ setSchema }: { setSchema: (schema: GraphQLSchema) => void }) { - setSchema(fusiongraph); - }, - // @ts-expect-error PromiseLike and Promise conflicts - onExecute({ args }) { - return mapMaybePromise(ensureFusiongraph(), () => { - args.schema ||= fusiongraph; - }); - }, - onContextBuilding({ extendContext }) { - const initialFusiongraph$ = ensureFusiongraph(); - function handleInitiatedFusiongraph() { - if (inContextSDK) { - extendContext(inContextSDK); - } - extendContext(opts.transportBaseContext as any); - } - if (isPromise(initialFusiongraph$)) { - return initialFusiongraph$.then(handleInitiatedFusiongraph); - } - handleInitiatedFusiongraph(); - }, - invalidateUnifiedGraph() { - return getAndSetFusiongraph(); - }, - }; -} diff --git a/packages/fusion/runtime/src/utils.ts b/packages/fusion/runtime/src/utils.ts index 2e921ff6155cd..8508392aebeb0 100644 --- a/packages/fusion/runtime/src/utils.ts +++ b/packages/fusion/runtime/src/utils.ts @@ -1,4 +1,4 @@ -import { ExecutionResult, GraphQLSchema } from 'graphql'; +import { DocumentNode, ExecutionResult, GraphQLSchema, print } from 'graphql'; import type { Transport, TransportBaseContext, @@ -6,12 +6,15 @@ import type { TransportExecutorFactoryFn, TransportExecutorFactoryOpts, } from '@graphql-mesh/transport-common'; +import { Logger } from '@graphql-mesh/types'; import { iterateAsync, mapMaybePromise } from '@graphql-mesh/utils'; import { ExecutionRequest, Executor, isAsyncIterable, + isDocumentNode, mapAsyncIterator, + printSchemaWithDirectives, type Maybe, type MaybePromise, } from '@graphql-tools/utils'; @@ -26,16 +29,26 @@ export type TransportsOption = transportKind: TTransportKind, ) => Promise> | Transport); -export function defaultTransportsOption(transportKind: string) { - return import(`@graphql-mesh/transport-${transportKind}`).catch(err => { - console.error(err); - throw new Error( - `No transport found for ${transportKind}. Please install @graphql-mesh/transport-${transportKind}`, - ); - }); +export function createDefaultTransportsOption(logger?: Logger) { + return function defaultTransportsOption(transportKind: string): Promise> { + const childLogger = logger?.child?.(transportKind); + return import(`@graphql-mesh/transport-${transportKind}`).catch(err => { + childLogger?.error(err); + throw new Error( + `No transport found for ${transportKind}. Please make sure you have installed @graphql-mesh/transport-${transportKind}`, + ); + }); + }; } -export function createTransportGetter(transports: TransportsOption) { +export type TransportGetter = ( + transportKind: TTransportKind, +) => MaybePromise>; + +/** + * Creates a function that returns the transport object for the given `transports` config option + */ +export function createTransportGetter(transports: TransportsOption): TransportGetter { if (typeof transports === 'function') { return transports; } @@ -51,173 +64,218 @@ export function createTransportGetter(transports: TransportsOption) { } export function getTransportExecutor( - transportGetter: ReturnType, + transportGetter: TransportGetter, transportContext: TransportExecutorFactoryOpts, + disposableStack: AsyncDisposableStack, ): MaybePromise { - transportContext.logger?.info(`Loading transport ${transportContext.transportEntry?.kind}`); - const transport$ = transportGetter(transportContext.transportEntry?.kind); - return mapMaybePromise(transport$, transport => transport.getSubgraphExecutor(transportContext)); + const transportKind = transportContext.transportEntry?.kind || ''; + const subgraphName = transportContext.subgraphName || ''; + transportContext.logger?.info(`Loading transport ${transportKind} for subgraph ${subgraphName}`); + return mapMaybePromise(transportGetter(transportKind), transport => + mapMaybePromise(transport.getSubgraphExecutor(transportContext), executor => { + if (isDisposable(executor)) { + disposableStack.use(executor); + } + return executor; + }), + ); } +/** + * This function creates a executor factory that uses the transport packages, + * and wraps them with the hooks + */ export function getOnSubgraphExecute({ - fusiongraph, - plugins, - transports, + onSubgraphExecuteHooks, transportBaseContext, transportEntryMap, - subgraphMap, + getSubgraphSchema, + disposableStack, + transports = createDefaultTransportsOption(transportBaseContext?.logger), }: { - fusiongraph: GraphQLSchema; - plugins?: FusiongraphPlugin[]; + onSubgraphExecuteHooks: OnSubgraphExecuteHook[]; transports?: TransportsOption; transportBaseContext?: TransportBaseContext; transportEntryMap?: Record; - subgraphMap: Map; + getSubgraphSchema(subgraphName: string): GraphQLSchema; + disposableStack: AsyncDisposableStack; }) { - const onSubgraphExecuteHooks: OnSubgraphExecuteHook[] = []; - if (plugins) { - for (const plugin of plugins) { - if (plugin.onSubgraphExecute) { - onSubgraphExecuteHooks.push(plugin.onSubgraphExecute); - } - } - } - const subgraphExecutorMap: Record = {}; + const subgraphExecutorMap = new Map(); const transportGetter = createTransportGetter(transports); - function onSubgraphExecute(subgraphName: string, executionRequest: ExecutionRequest) { - let executor: Executor = subgraphExecutorMap[subgraphName]; + + return function onSubgraphExecute(subgraphName: string, executionRequest: ExecutionRequest) { + let executor: Executor = subgraphExecutorMap.get(subgraphName); + // If the executor is not initialized yet, initialize it if (executor == null) { transportBaseContext?.logger?.info(`Initializing executor for subgraph ${subgraphName}`); - const transportEntry = transportEntryMap[subgraphName]; - // eslint-disable-next-line no-inner-declarations - function wrapExecutorWithHooks(currentExecutor: Executor) { - if (onSubgraphExecuteHooks.length) { - return function executorWithHooks(executionRequest: ExecutionRequest) { - const onSubgraphExecuteDoneHooks: OnSubgraphExecuteDoneHook[] = []; - const subgraph = subgraphMap.get(subgraphName); - const onSubgraphExecuteHooksRes$ = iterateAsync( - onSubgraphExecuteHooks, - onSubgraphExecuteHook => - onSubgraphExecuteHook({ - fusiongraph, - subgraph, + // Lazy executor that loads transport executor on demand + executor = function lazyExecutor(subgraphExecReq: ExecutionRequest) { + return mapMaybePromise( + // Gets the transport executor for the given subgraph + getTransportExecutor( + transportGetter, + transportBaseContext + ? { + ...transportBaseContext, subgraphName, - transportEntry, - executionRequest, - setExecutionRequest(newExecutionRequest) { - executionRequest = newExecutionRequest; + get subgraph() { + return getSubgraphSchema(subgraphName); }, - executor: currentExecutor, - setExecutor(newExecutor) { - currentExecutor = newExecutor; + get transportEntry() { + return transportEntryMap?.[subgraphName]; }, - }), + } + : { + get subgraph() { + return getSubgraphSchema(subgraphName); + }, + get transportEntry() { + return transportEntryMap?.[subgraphName]; + }, + subgraphName, + }, + disposableStack, + ), + executor_ => { + // Wraps the transport executor with hooks + executor = wrapExecutorWithHooks({ + executor: executor_, + onSubgraphExecuteHooks, + subgraphName, + transportEntryMap, + getSubgraphSchema, + }); + // Caches the executor for future use + subgraphExecutorMap.set(subgraphName, executor); + return executor(subgraphExecReq); + }, + ); + }; + // Caches the lazy executor to prevent race conditions + subgraphExecutorMap.set(subgraphName, executor); + } + return executor(executionRequest); + }; +} + +export interface WrapExecuteWithHooksOptions { + executor: Executor; + onSubgraphExecuteHooks: OnSubgraphExecuteHook[]; + subgraphName: string; + transportEntryMap?: Record; + getSubgraphSchema: (subgraphName: string) => GraphQLSchema; +} + +/** + * This function wraps the executor created by the transport package + * with `onSubgraphExecuteHooks` to hook into the execution phase of subgraphs + */ +export function wrapExecutorWithHooks({ + executor, + onSubgraphExecuteHooks, + subgraphName, + transportEntryMap, + getSubgraphSchema, +}: WrapExecuteWithHooksOptions): Executor { + if (onSubgraphExecuteHooks.length === 0) { + return executor; + } + return function executorWithHooks(executionRequest: ExecutionRequest) { + const onSubgraphExecuteDoneHooks: OnSubgraphExecuteDoneHook[] = []; + return mapMaybePromise( + iterateAsync( + onSubgraphExecuteHooks, + onSubgraphExecuteHook => + onSubgraphExecuteHook({ + get subgraph() { + return getSubgraphSchema(subgraphName); + }, + subgraphName, + get transportEntry() { + return transportEntryMap?.[subgraphName]; + }, + executionRequest, + setExecutionRequest(newExecutionRequest) { + executionRequest = newExecutionRequest; + }, + executor, + setExecutor(newExecutor) { + executor = newExecutor; + }, + }), + onSubgraphExecuteDoneHooks, + ), + () => { + if (onSubgraphExecuteDoneHooks.length === 0) { + return executor(executionRequest); + } + return mapMaybePromise(executor(executionRequest), currentResult => { + const executeDoneResults: OnSubgraphExecuteDoneResult[] = []; + return mapMaybePromise( + iterateAsync( onSubgraphExecuteDoneHooks, - ); - function handleOnSubgraphExecuteHooksResult() { - if (onSubgraphExecuteDoneHooks.length) { - // eslint-disable-next-line no-inner-declarations - function handleExecutorResWithHooks( - currentResult: ExecutionResult | AsyncIterable, - ) { - const executeDoneResults: OnSubgraphExecuteDoneResult[] = []; - const onSubgraphExecuteDoneHooksRes$ = iterateAsync( - onSubgraphExecuteDoneHooks, - onSubgraphExecuteDoneHook => - onSubgraphExecuteDoneHook({ - result: currentResult, - setResult(newResult: ExecutionResult) { - currentResult = newResult; - }, - }), - executeDoneResults, - ); - function handleExecuteDoneResults( - result: AsyncIterable | ExecutionResult, - ) { - if (!isAsyncIterable(result)) { - return result; - } + onSubgraphExecuteDoneHook => + onSubgraphExecuteDoneHook({ + result: currentResult, + setResult(newResult: ExecutionResult) { + currentResult = newResult; + }, + }), + executeDoneResults, + ), + () => { + if (!isAsyncIterable(currentResult)) { + return currentResult; + } - if (executeDoneResults.length === 0) { - return result; - } + if (executeDoneResults.length === 0) { + return currentResult; + } - const onNextHooks: OnSubgraphExecuteDoneResultOnNext[] = []; - const onEndHooks: OnSubgraphExecuteDoneResultOnEnd[] = []; - for (const executeDoneResult of executeDoneResults) { - if (executeDoneResult.onNext) { - onNextHooks.push(executeDoneResult.onNext); - } - if (executeDoneResult.onEnd) { - onEndHooks.push(executeDoneResult.onEnd); - } - } + const onNextHooks: OnSubgraphExecuteDoneResultOnNext[] = []; + const onEndHooks: OnSubgraphExecuteDoneResultOnEnd[] = []; - return mapAsyncIterator( - result[Symbol.asyncIterator](), - currentResult => { - if (onNextHooks.length === 0) { - return currentResult; - } - const $ = iterateAsync(onNextHooks, onNext => - onNext({ - result: currentResult, - setResult: res => { - currentResult = res; - }, - }), - ); - return mapMaybePromise($, () => currentResult); - }, - undefined, - () => - onEndHooks.length === 0 - ? undefined - : iterateAsync(onEndHooks, onEnd => onEnd()), - ); - } - return mapMaybePromise(onSubgraphExecuteDoneHooksRes$, () => - handleExecuteDoneResults(currentResult), - ); + for (const executeDoneResult of executeDoneResults) { + if (executeDoneResult.onNext) { + onNextHooks.push(executeDoneResult.onNext); + } + if (executeDoneResult.onEnd) { + onEndHooks.push(executeDoneResult.onEnd); } - const executorRes$ = currentExecutor(executionRequest); - return mapMaybePromise(executorRes$, handleExecutorResWithHooks); } - return currentExecutor(executionRequest); - } - return mapMaybePromise(onSubgraphExecuteHooksRes$, handleOnSubgraphExecuteHooksResult); - }; - } - return currentExecutor; - } - executor = function lazyExecutor(subgraphExecReq: ExecutionRequest) { - const subgraph = subgraphMap.get(subgraphName); - const executor$ = getTransportExecutor( - transportGetter, - transportBaseContext - ? { - ...transportBaseContext, - subgraphName, - subgraph, - transportEntry, + + if (onNextHooks.length === 0 && onEndHooks.length === 0) { + return currentResult; } - : { subgraph, transportEntry, subgraphName }, - ); - return mapMaybePromise(executor$, executor_ => { - executor = wrapExecutorWithHooks(executor_) as Executor; - subgraphExecutorMap[subgraphName] = executor; - return executor(subgraphExecReq); - }); - }; - } - return executor(executionRequest); - } - return onSubgraphExecute; + const asyncIterator = currentResult[Symbol.asyncIterator](); + return mapAsyncIterator( + asyncIterator, + currentResult => + mapMaybePromise( + iterateAsync(onNextHooks, onNext => + onNext({ + result: currentResult, + setResult: res => { + currentResult = res; + }, + }), + ), + () => currentResult, + ), + undefined, + () => + onEndHooks.length === 0 ? undefined : iterateAsync(onEndHooks, onEnd => onEnd()), + ); + }, + ); + }); + }, + ); + }; } -export interface FusiongraphPlugin { +export interface UnifiedGraphPlugin { onSubgraphExecute?: OnSubgraphExecuteHook; } @@ -226,7 +284,6 @@ export type OnSubgraphExecuteHook = ( ) => Promise> | Maybe; export interface OnSubgraphExecutePayload { - fusiongraph: GraphQLSchema; subgraph: GraphQLSchema; subgraphName: string; transportEntry?: TransportEntry; @@ -260,3 +317,30 @@ export type OnSubgraphExecuteDoneResult = { onNext?: OnSubgraphExecuteDoneResultOnNext; onEnd?: OnSubgraphExecuteDoneResultOnEnd; }; + +export function compareSchemas( + a: DocumentNode | string | GraphQLSchema, + b: DocumentNode | string | GraphQLSchema, +) { + let aStr: string; + if (typeof a === 'string') { + aStr = a; + } else if (isDocumentNode(a)) { + aStr = print(a); + } else { + aStr = printSchemaWithDirectives(a); + } + let bStr: string; + if (typeof b === 'string') { + bStr = b; + } else if (isDocumentNode(b)) { + bStr = print(b); + } else { + bStr = printSchemaWithDirectives(b); + } + return aStr === bStr; +} + +export function isDisposable(obj: any): obj is Disposable | AsyncDisposable { + return obj?.[Symbol.dispose] != null || obj?.[Symbol.asyncDispose] != null; +} diff --git a/packages/fusion/runtime/tests/polling.test.ts b/packages/fusion/runtime/tests/polling.test.ts new file mode 100644 index 0000000000000..607e8786f97e2 --- /dev/null +++ b/packages/fusion/runtime/tests/polling.test.ts @@ -0,0 +1,43 @@ +import { buildSchema } from 'graphql'; +import { composeSubgraphs } from '@graphql-mesh/fusion-composition'; +import { UnifiedGraphManager } from '../src/unifiedGraphManager'; + +describe('Polling', () => { + it('polls the schema in a certain interval', async () => { + jest.useFakeTimers(); + const pollingInterval = 35_000; + const unifiedGraphFetcher = () => + composeSubgraphs([ + { + name: 'Test', + schema: buildSchema(/* GraphQL */ ` + """ + Fetched on ${new Date().toISOString()} + """ + type Query { + test: String + } + `), + }, + ]); + await using manager = new UnifiedGraphManager({ + getUnifiedGraph: unifiedGraphFetcher, + polling: pollingInterval, + }); + async function getFetchedTime() { + const schema = await manager.getUnifiedGraph(); + const queryType = schema.getQueryType(); + const lastFetchedDateStr = queryType.description.match(/Fetched on (.*)/)[1]; + const lastFetchedDate = new Date(lastFetchedDateStr); + return lastFetchedDate; + } + const firstDate = await getFetchedTime(); + jest.advanceTimersByTime(pollingInterval); + const secondDate = await getFetchedTime(); + expect(secondDate.getTime() - firstDate.getTime()).toBeGreaterThanOrEqual(pollingInterval); + jest.advanceTimersByTime(pollingInterval); + const thirdDate = await getFetchedTime(); + expect(thirdDate.getTime() - secondDate.getTime()).toBeGreaterThanOrEqual(pollingInterval); + expect(thirdDate.getTime() - firstDate.getTime()).toBeGreaterThanOrEqual(pollingInterval * 2); + }); +}); diff --git a/packages/fusion/runtime/tests/transforms/hoist-field.test.ts b/packages/fusion/runtime/tests/transforms/hoist-field.test.ts index 1e797b9b18480..a2018b5b15da0 100644 --- a/packages/fusion/runtime/tests/transforms/hoist-field.test.ts +++ b/packages/fusion/runtime/tests/transforms/hoist-field.test.ts @@ -1,4 +1,4 @@ -import { buildSchema, GraphQLField, GraphQLObjectType, printSchema } from 'graphql'; +import { GraphQLField, GraphQLObjectType, printSchema } from 'graphql'; import { createHoistFieldTransform } from '@graphql-mesh/fusion-composition'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { composeAndGetExecutor, composeAndGetPublicSchema } from '../utils'; diff --git a/packages/fusion/runtime/tests/transforms/naming-convention.test.ts b/packages/fusion/runtime/tests/transforms/naming-convention.test.ts index 802ce6ad295af..43d715c5e45e1 100644 --- a/packages/fusion/runtime/tests/transforms/naming-convention.test.ts +++ b/packages/fusion/runtime/tests/transforms/naming-convention.test.ts @@ -1,14 +1,6 @@ -import { - buildSchema, - GraphQLEnumType, - GraphQLObjectType, - GraphQLUnionType, - validate, -} from 'graphql'; +import { buildSchema, GraphQLEnumType, GraphQLObjectType, GraphQLUnionType } from 'graphql'; import { createNamingConventionTransform } from '@graphql-mesh/fusion-composition'; -import { createDefaultExecutor } from '@graphql-tools/delegate'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { getExecutorForFusiongraph } from '../../src/useFusiongraph'; import { composeAndGetExecutor, composeAndGetPublicSchema, expectTheSchemaSDLToBe } from '../utils'; describe('Naming Convention', () => { diff --git a/packages/fusion/runtime/tests/utils.ts b/packages/fusion/runtime/tests/utils.ts index f228048995a67..f56b0741f6ded 100644 --- a/packages/fusion/runtime/tests/utils.ts +++ b/packages/fusion/runtime/tests/utils.ts @@ -1,52 +1,81 @@ import { - buildClientSchema, buildSchema, - getIntrospectionQuery, GraphQLSchema, lexicographicSortSchema, + parse, printSchema, validate, } from 'graphql'; import { composeSubgraphs, SubgraphConfig } from '@graphql-mesh/fusion-composition'; -import { mapMaybePromise } from '@graphql-mesh/utils'; import { createDefaultExecutor } from '@graphql-tools/delegate'; -import { getExecutorForFusiongraph } from '../src/useFusiongraph'; +import { normalizedExecutor } from '@graphql-tools/executor'; +import { isAsyncIterable } from '@graphql-tools/utils'; +import { UnifiedGraphManager } from '../src/unifiedGraphManager.js'; export function composeAndGetPublicSchema(subgraphs: SubgraphConfig[]) { - const executor = composeAndGetExecutor(subgraphs); - return mapMaybePromise( - executor({ - query: getIntrospectionQuery(), - }), - introspection => buildClientSchema(introspection), - ); + const manager = new UnifiedGraphManager({ + getUnifiedGraph: () => composeSubgraphs(subgraphs), + transports() { + return { + getSubgraphExecutor({ subgraphName }) { + return createDefaultExecutor( + subgraphs.find(subgraph => subgraph.name === subgraphName).schema, + ); + }, + }; + }, + }); + return manager.getUnifiedGraph(); } export function composeAndGetExecutor(subgraphs: SubgraphConfig[]) { const fusiongraph = composeSubgraphs(subgraphs); - return getExecutorForFusiongraph({ - getFusiongraph: () => fusiongraph, + const manager = new UnifiedGraphManager({ + getUnifiedGraph: () => fusiongraph, transports() { return { getSubgraphExecutor({ subgraphName }) { - const subgraphConfig = subgraphs.find(s => s.name === subgraphName); - if (!subgraphConfig) { - throw new Error(`Subgraph ${subgraphName} not found`); - } - const subgraphExecutor = createDefaultExecutor(subgraphConfig.schema); - return function defaultExecutor(req) { - const validationErrors = validate(subgraphConfig.schema, req.document); - if (validationErrors.length) { - return { - errors: validationErrors, - }; - } - return subgraphExecutor(req); - }; + return createDefaultExecutor( + subgraphs.find(subgraph => subgraph.name === subgraphName).schema, + ); }, }; }, }); + return async function testExecutor({ + query, + variables, + }: { + query: string; + variables?: Record; + }) { + const document = parse(query); + const schema = await manager.getUnifiedGraph(); + const validationErrors = validate(schema, document); + if (validationErrors.length === 1) { + throw validationErrors[0]; + } + if (validationErrors.length > 1) { + throw new AggregateError(validationErrors); + } + const context = await manager.getContext(); + const res = await normalizedExecutor({ + schema, + document, + contextValue: context, + variableValues: variables, + }); + if (isAsyncIterable(res)) { + throw new Error('AsyncIterable is not supported'); + } + if (res.errors?.length === 1) { + throw res.errors[0]; + } + if (res.errors?.length > 1) { + throw new AggregateError(res.errors); + } + return res.data; + }; } export function expectTheSchemaSDLToBe(schema: GraphQLSchema, sdl: string) { diff --git a/packages/legacy/handlers/mysql/src/index.ts b/packages/legacy/handlers/mysql/src/index.ts index 65e184eb57a77..a6f820045c4b7 100644 --- a/packages/legacy/handlers/mysql/src/index.ts +++ b/packages/legacy/handlers/mysql/src/index.ts @@ -20,14 +20,12 @@ export default class MySQLHandler implements MeshHandler { private pubsub: MeshPubSub; private importFn: ImportFn; private schemaProxy: StoreProxy; - private logger: Logger; constructor({ name, config, baseDir, pubsub, - logger, store, importFn, }: MeshHandlerOptions) { @@ -36,7 +34,6 @@ export default class MySQLHandler implements MeshHandler { this.baseDir = baseDir; this.pubsub = pubsub; this.importFn = importFn; - this.logger = logger; this.schemaProxy = store.proxy( 'schema.graphql', PredefinedProxyOptions.GraphQLSchemaWithDiffing, @@ -83,14 +80,19 @@ export default class MySQLHandler implements MeshHandler { }) : configPool; + const executor = getMySQLExecutor({ + subgraph: schema, + pool, + }); + + const id = this.pubsub.subscribe('destroy', () => { + executor[Symbol.asyncDispose](); + this.pubsub.unsubscribe(id); + }); + return { schema, - executor: getMySQLExecutor({ - subgraph: schema, - pool, - pubsub: this.pubsub, - logger: this.logger, - }), + executor, }; } } diff --git a/packages/legacy/handlers/neo4j/src/index.ts b/packages/legacy/handlers/neo4j/src/index.ts index b28e92e902493..32e1d1e39cc76 100644 --- a/packages/legacy/handlers/neo4j/src/index.ts +++ b/packages/legacy/handlers/neo4j/src/index.ts @@ -100,6 +100,11 @@ export default class Neo4JHandler implements MeshHandler { logger: this.logger, }); + const id = this.pubsub.subscribe('destroy', () => { + executor[Symbol.asyncDispose](); + this.pubsub.unsubscribe(id); + }); + return { schema, executor, diff --git a/packages/legacy/handlers/soap/src/index.ts b/packages/legacy/handlers/soap/src/index.ts index 4702f37b7fe57..3db41e9ac2229 100644 --- a/packages/legacy/handlers/soap/src/index.ts +++ b/packages/legacy/handlers/soap/src/index.ts @@ -60,6 +60,7 @@ export default class SoapHandler implements MeshHandler { const soapLoader = new SOAPLoader({ subgraphName: this.name, fetch: fetchFn, + logger: this.logger, schemaHeaders: this.config.schemaHeaders, operationHeaders: this.config.operationHeaders, }); diff --git a/packages/legacy/handlers/supergraph/src/index.ts b/packages/legacy/handlers/supergraph/src/index.ts index 8a89a32593ad1..94c4aef166cd0 100644 --- a/packages/legacy/handlers/supergraph/src/index.ts +++ b/packages/legacy/handlers/supergraph/src/index.ts @@ -109,8 +109,9 @@ export default class SupergraphHandler implements MeshHandler { } const schema = getStitchedSchemaFromSupergraphSdl({ supergraphSdl, - onSubschemaConfig(subgraphConfig) { - let { name: subgraphName, endpoint: nonInterpolatedEndpoint } = subgraphConfig; + onSubschemaConfig(subschemaConfig) { + const subgraphName = subschemaConfig.name; + let nonInterpolatedEndpoint = subschemaConfig.endpoint; const subgraphRealName = subgraphNameIdMap.get(subgraphName); const subgraphConfiguration: YamlConfig.SubgraphConfiguration = subgraphConfigs.find( subgraphConfig => subgraphConfig.name === subgraphRealName, @@ -119,7 +120,7 @@ export default class SupergraphHandler implements MeshHandler { }; nonInterpolatedEndpoint = subgraphConfiguration.endpoint || nonInterpolatedEndpoint; const endpointFactory = getInterpolatedStringFactory(nonInterpolatedEndpoint); - subgraphConfig.executor = buildHTTPExecutor({ + subschemaConfig.executor = buildHTTPExecutor({ ...(subgraphConfiguration as any), endpoint: nonInterpolatedEndpoint, fetch(url: string, init: any, context: any, info: any) { diff --git a/packages/legacy/runtime/src/get-mesh.ts b/packages/legacy/runtime/src/get-mesh.ts index f36d0f5b8d0e2..9ebdcc60feeb4 100644 --- a/packages/legacy/runtime/src/get-mesh.ts +++ b/packages/legacy/runtime/src/get-mesh.ts @@ -30,6 +30,7 @@ import { DefaultLogger, getHeadersObj, groupTransforms, + mapMaybePromise, parseWithCache, PubSub, wrapFetchWithHooks, @@ -41,7 +42,6 @@ import { ExecutionResult, getRootTypeMap, isAsyncIterable, - isPromise, mapAsyncIterator, memoize1, } from '@graphql-tools/utils'; @@ -363,44 +363,34 @@ export async function getMesh(options: GetMeshOptions): Promise { operationName?: string, ) { const document = typeof documentOrSDL === 'string' ? parse(documentOrSDL) : documentOrSDL; - const contextValue$ = contextFactory(contextValue); const operationAST = getOperationAST(document, operationName); if (!operationAST) { throw new Error(`Cannot execute a request without a valid operation.`); } const isSubscription = operationAST.operation === 'subscription'; const executeFn = isSubscription ? subscribe : execute; - if (isPromise(contextValue$)) { - return contextValue$.then(contextValue => - executeFn({ - schema, - document, - contextValue, - rootValue, - variableValues: variableValues as any, - operationName, - }), - ); - } - return executeFn({ - schema, - document, - contextValue: contextValue$, - rootValue, - variableValues: variableValues as any, - operationName, - }); + return mapMaybePromise(contextFactory(contextValue), contextValue => + executeFn({ + schema, + document, + contextValue, + rootValue, + variableValues, + operationName, + }), + ); } as MeshExecutor; } function sdkRequesterFactory(globalContext: any): SdkRequester { const executor = createExecutor(globalContext); return function sdkRequester(...args) { - const result$ = executor(...args); - if (isPromise(result$)) { - return result$.then(handleExecutorResultForSdk); - } - return handleExecutorResultForSdk(result$); + return mapMaybePromise(executor(...args), function handleExecutorResultForSdk(result) { + if (isAsyncIterable(result)) { + return mapAsyncIterator(result as AsyncIterableIterator, extractDataOrThrowErrors); + } + return extractDataOrThrowErrors(result); + }); }; } @@ -432,13 +422,6 @@ export async function getMesh(options: GetMeshOptions): Promise { }; } -function handleExecutorResultForSdk(result: Awaited>) { - if (isAsyncIterable(result)) { - return mapAsyncIterator(result as AsyncIterableIterator, extractDataOrThrowErrors); - } - return extractDataOrThrowErrors(result); -} - function extractDataOrThrowErrors(result: ExecutionResult): T { if (result.errors) { if (result.errors.length === 1) { diff --git a/packages/legacy/runtime/src/in-context-sdk.ts b/packages/legacy/runtime/src/in-context-sdk.ts index fab0fd17d5033..ba319ff1b8969 100644 --- a/packages/legacy/runtime/src/in-context-sdk.ts +++ b/packages/legacy/runtime/src/in-context-sdk.ts @@ -21,7 +21,7 @@ import { SelectionSetParam, SelectionSetParamOrFactory, } from '@graphql-mesh/types'; -import { iterateAsync, parseWithCache } from '@graphql-mesh/utils'; +import { iterateAsync, mapMaybePromise, parseWithCache } from '@graphql-mesh/utils'; import { BatchDelegateOptions, batchDelegateToSchema } from '@graphql-tools/batch-delegate'; import { delegateToSchema, @@ -29,12 +29,7 @@ import { StitchingInfo, SubschemaConfig, } from '@graphql-tools/delegate'; -import { - buildOperationNodeForField, - isDocumentNode, - isPromise, - memoize1, -} from '@graphql-tools/utils'; +import { buildOperationNodeForField, isDocumentNode, memoize1 } from '@graphql-tools/utils'; import { WrapQuery } from '@graphql-tools/wrap'; import { MESH_API_CONTEXT_SYMBOL } from './constants.js'; @@ -219,19 +214,12 @@ export function getInContextSDK( onDelegateHook => onDelegateHook(onDelegatePayload), onDelegateHookDones, ); - if (isPromise(onDelegateResult$)) { - return onDelegateResult$.then(() => - handleIterationResult( - batchDelegateToSchema, - batchDelegationOptions, - onDelegateHookDones, - ), - ); - } - return handleIterationResult( - batchDelegateToSchema, - batchDelegationOptions, - onDelegateHookDones, + return mapMaybePromise(onDelegateResult$, () => + handleIterationResult( + batchDelegateToSchema, + batchDelegationOptions, + onDelegateHookDones, + ), ); } else { const regularDelegateOptions: IDelegateToSchemaOptions = { @@ -260,19 +248,12 @@ export function getInContextSDK( onDelegateHook => onDelegateHook(onDelegatePayload), onDelegateHookDones, ); - if (isPromise(onDelegateResult$)) { - return onDelegateResult$.then(() => - handleIterationResult( - delegateToSchema, - regularDelegateOptions, - onDelegateHookDones, - ), - ); - } - return handleIterationResult( - delegateToSchema, - regularDelegateOptions, - onDelegateHookDones, + return mapMaybePromise(onDelegateResult$, () => + handleIterationResult( + delegateToSchema, + regularDelegateOptions, + onDelegateHookDones, + ), ); } }; @@ -334,26 +315,16 @@ function handleIterationResult any>( onDelegateHookDones: OnDelegateHookDone[], ) { const delegationResult$ = delegateFn(delegateOptions); - if (isPromise(delegationResult$)) { - return delegationResult$.then(delegationResult => - handleOnDelegateDone(delegationResult, onDelegateHookDones), + return mapMaybePromise(delegationResult$, function handleOnDelegateDone(delegationResult) { + function setResult(newResult: any) { + delegationResult = newResult; + } + const onDelegateDoneResult$ = iterateAsync(onDelegateHookDones, onDelegateHookDone => + onDelegateHookDone({ + result: delegationResult, + setResult, + }), ); - } - return handleOnDelegateDone(delegationResult$, onDelegateHookDones); -} - -function handleOnDelegateDone(delegationResult: any, onDelegateHookDones: OnDelegateHookDone[]) { - function setResult(newResult: any) { - delegationResult = newResult; - } - const onDelegateDoneResult$ = iterateAsync(onDelegateHookDones, onDelegateHookDone => - onDelegateHookDone({ - result: delegationResult, - setResult, - }), - ); - if (isPromise(onDelegateDoneResult$)) { - return onDelegateDoneResult$.then(() => delegationResult); - } - return delegationResult; + return mapMaybePromise(onDelegateDoneResult$, () => delegationResult); + }); } diff --git a/packages/legacy/runtime/src/useSubschema.ts b/packages/legacy/runtime/src/useSubschema.ts index 171929c5ddcb9..1e7d55cd687ac 100644 --- a/packages/legacy/runtime/src/useSubschema.ts +++ b/packages/legacy/runtime/src/useSubschema.ts @@ -2,7 +2,11 @@ import { BREAK, DocumentNode, execute, FieldNode, OperationDefinitionNode, visit import { CompiledQuery, compileQuery, isCompiledQuery } from 'graphql-jit'; import { mapAsyncIterator, Plugin, TypedExecutionArgs } from '@envelop/core'; import { ExecutionResultWithSerializer } from '@envelop/graphql-jit'; -import { applyRequestTransforms, applyResultTransforms } from '@graphql-mesh/utils'; +import { + applyRequestTransforms, + applyResultTransforms, + mapMaybePromise, +} from '@graphql-mesh/utils'; import { applySchemaTransforms, createDefaultExecutor, @@ -15,7 +19,6 @@ import { getDefinedRootType, getOperationASTFromRequest, isAsyncIterable, - isPromise, MaybeAsyncIterable, MaybePromise, memoize1, @@ -123,33 +126,29 @@ function getExecuteFn(subschema: Subschema) { compiledQueryCache.set(request.document, compiledQuery); } if (operationAST.operation === 'subscription') { - const result$ = compiledQuery.subscribe( - request.rootValue, - request.context, - request.variables, - ) as MaybePromise; - if (isPromise(result$)) { - return result$.then(result => { + return mapMaybePromise( + compiledQuery.subscribe( + request.rootValue, + request.context, + request.variables, + ) as MaybePromise, + result => { result.stringify = compiledQuery.stringify; return result; - }); - } - result$.stringify = compiledQuery.stringify; - return result$; + }, + ); } - const result$ = compiledQuery.query( - request.rootValue, - request.context, - request.variables, - ) as MaybePromise; - if (isPromise(result$)) { - return result$.then(result => { + return mapMaybePromise( + compiledQuery.query( + request.rootValue, + request.context, + request.variables, + ) as MaybePromise, + result => { result.stringify = compiledQuery.stringify; return result; - }); - } - result$.stringify = compiledQuery.stringify; - return result$; + }, + ); }; } } @@ -173,30 +172,29 @@ function getExecuteFn(subschema: Subschema) { } else { transformedDocumentNodeCache.set(originalRequest.document, transformedRequest.document); } - function handleResult(originalResult: MaybeAsyncIterable) { - if (isAsyncIterable(originalResult)) { - return mapAsyncIterator(originalResult, singleResult => - applyResultTransforms( - singleResult, - delegationContext, - transformationContext, - subschema.transforms, - ), + + return mapMaybePromise( + executor(transformedRequest), + function handleResult(originalResult: MaybeAsyncIterable) { + if (isAsyncIterable(originalResult)) { + return mapAsyncIterator(originalResult, singleResult => + applyResultTransforms( + singleResult, + delegationContext, + transformationContext, + subschema.transforms, + ), + ); + } + const transformedResult = applyResultTransforms( + originalResult, + delegationContext, + transformationContext, + subschema.transforms, ); - } - const transformedResult = applyResultTransforms( - originalResult, - delegationContext, - transformationContext, - subschema.transforms, - ); - return transformedResult; - } - const originalResult$ = executor(transformedRequest); - if (isPromise(originalResult$)) { - return originalResult$.then(handleResult); - } - return handleResult(originalResult$); + return transformedResult; + }, + ); }; } diff --git a/packages/legacy/runtime/test/getMesh.test.ts b/packages/legacy/runtime/test/getMesh.test.ts index 31236fca2b754..65692007cd7ec 100644 --- a/packages/legacy/runtime/test/getMesh.test.ts +++ b/packages/legacy/runtime/test/getMesh.test.ts @@ -4,7 +4,8 @@ import LocalforageCache from '@graphql-mesh/cache-localforage'; import GraphQLHandler from '@graphql-mesh/graphql'; import StitchingMerger from '@graphql-mesh/merger-stitching'; import { InMemoryStoreStorageAdapter, MeshStore } from '@graphql-mesh/store'; -import { defaultImportFn, DefaultLogger, PubSub } from '@graphql-mesh/utils'; +import { Logger } from '@graphql-mesh/types'; +import { defaultImportFn, PubSub } from '@graphql-mesh/utils'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { printSchemaWithDirectives } from '@graphql-tools/utils'; import { getMesh } from '../src/get-mesh.js'; @@ -15,7 +16,7 @@ describe('getMesh', () => { let cache: LocalforageCache; let pubsub: PubSub; let store: MeshStore; - let logger: DefaultLogger; + let logger: Logger; let merger: StitchingMerger; beforeEach(() => { cache = new LocalforageCache(); @@ -24,7 +25,14 @@ describe('getMesh', () => { readonly: false, validate: false, }); - logger = new DefaultLogger('Mesh Test'); + logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + log: jest.fn(), + child: () => logger, + }; merger = new StitchingMerger({ store, cache, diff --git a/packages/legacy/types/src/index.ts b/packages/legacy/types/src/index.ts index 18b28b64fd53a..d7b4bfad28248 100644 --- a/packages/legacy/types/src/index.ts +++ b/packages/legacy/types/src/index.ts @@ -157,7 +157,7 @@ export type OnDelegateHookDone = (payload: OnDelegateHookDonePayload) => Promise export type MeshPlugin = Plugin & { onFetch?: OnFetchHook; onDelegate?: OnDelegateHook; -}; +} & Partial; export type MeshFetch = ( url: string, diff --git a/packages/legacy/utils/package.json b/packages/legacy/utils/package.json index d2070f9243299..58c356bb6aff6 100644 --- a/packages/legacy/utils/package.json +++ b/packages/legacy/utils/package.json @@ -42,6 +42,7 @@ "@graphql-mesh/string-interpolation": "^0.5.4", "@graphql-tools/delegate": "^10.0.11", "@whatwg-node/fetch": "^0.9.13", + "disposablestack": "^1.1.6", "dset": "^3.1.2", "js-yaml": "^4.1.0", "lodash.get": "^4.4.2", diff --git a/packages/legacy/utils/src/iterateAsync.ts b/packages/legacy/utils/src/iterateAsync.ts index 2e2b363a5d8af..5d3b0f040200a 100644 --- a/packages/legacy/utils/src/iterateAsync.ts +++ b/packages/legacy/utils/src/iterateAsync.ts @@ -1,4 +1,5 @@ -import { isPromise, type MaybePromise } from '@graphql-tools/utils'; +import { type MaybePromise } from '@graphql-tools/utils'; +import { mapMaybePromise } from './map-maybe-promise.js'; export function iterateAsync( iterable: Iterable, @@ -11,19 +12,12 @@ export function iterateAsync( if (endOfIterator) { return; } - const result$ = callback(value); - if (isPromise(result$)) { - return result$.then(result => { - if (result) { - results?.push(result); - } - return iterate(); - }); - } - if (result$) { - results?.push(result$); - } - return iterate(); + return mapMaybePromise(callback(value), result => { + if (result) { + results?.push(result); + } + return iterate(); + }); } return iterate(); } diff --git a/packages/legacy/utils/src/registerTerminateHandler.ts b/packages/legacy/utils/src/registerTerminateHandler.ts index ae80f631c0acd..3115d09ad15c6 100644 --- a/packages/legacy/utils/src/registerTerminateHandler.ts +++ b/packages/legacy/utils/src/registerTerminateHandler.ts @@ -1,3 +1,5 @@ +import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; + const terminateEvents = ['SIGINT', 'SIGTERM'] as const; export type TerminateEvents = (typeof terminateEvents)[number]; @@ -28,3 +30,13 @@ export function registerTerminateHandler(callback: TerminateHandler) { terminateHandlers.delete(callback); }; } + +let terminateStack: AsyncDisposableStack; + +export function getTerminateStack() { + if (!terminateStack) { + terminateStack = new AsyncDisposableStack(); + registerTerminateHandler(() => terminateStack.disposeAsync()); + } + return terminateStack; +} diff --git a/packages/legacy/utils/src/wrapFetchWithHooks.ts b/packages/legacy/utils/src/wrapFetchWithHooks.ts index 1e4b17171ad44..b806768b38185 100644 --- a/packages/legacy/utils/src/wrapFetchWithHooks.ts +++ b/packages/legacy/utils/src/wrapFetchWithHooks.ts @@ -1,62 +1,54 @@ import { MeshFetch, OnFetchHook, OnFetchHookDone } from '@graphql-mesh/types'; -import { isPromise } from '@graphql-tools/utils'; import { iterateAsync } from './iterateAsync.js'; +import { mapMaybePromise } from './map-maybe-promise.js'; export function wrapFetchWithHooks(onFetchHooks: OnFetchHook[]): MeshFetch { return function wrappedFetchFn(url, options, context, info) { let fetchFn: MeshFetch; - const doneHooks: OnFetchHookDone[] = []; - function setFetchFn(newFetchFn: MeshFetch) { - fetchFn = newFetchFn; - } - const result$ = iterateAsync( - onFetchHooks, - onFetch => - onFetch({ - fetchFn, - setFetchFn, - url, - setURL(newUrl) { - url = String(newUrl); - }, - options, - setOptions(newOptions) { - options = newOptions; - }, - context, - info, - }), - doneHooks, + const onFetchDoneHooks: OnFetchHookDone[] = []; + return mapMaybePromise( + iterateAsync( + onFetchHooks, + onFetch => + onFetch({ + fetchFn, + setFetchFn(newFetchFn) { + fetchFn = newFetchFn; + }, + url, + setURL(newUrl) { + url = String(newUrl); + }, + options, + setOptions(newOptions) { + options = newOptions; + }, + context, + info, + }), + onFetchDoneHooks, + ), + function handleIterationResult() { + const res$ = fetchFn(url, options, context, info); + if (onFetchDoneHooks.length === 0) { + return res$; + } + return mapMaybePromise(res$, function (response: Response) { + return mapMaybePromise( + iterateAsync(onFetchDoneHooks, onFetchDone => + onFetchDone({ + response, + setResponse(newResponse) { + response = newResponse; + }, + }), + ), + function handleOnFetchDone() { + return response; + }, + ); + }); + }, ); - function handleIterationResult() { - const response$ = fetchFn(url, options, context, info); - if (doneHooks.length === 0) { - return response$; - } - if (isPromise(response$)) { - return response$.then(response => handleOnFetchDone(response, doneHooks)); - } - return handleOnFetchDone(response$, doneHooks); - } - if (isPromise(result$)) { - return result$.then(handleIterationResult); - } - return handleIterationResult(); } as MeshFetch; } - -function handleOnFetchDone(response: Response, onFetchDoneHooks: OnFetchHookDone[]) { - function setResponse(newResponse: Response) { - response = newResponse; - } - const result$ = iterateAsync(onFetchDoneHooks, onFetchDone => - onFetchDone({ - response, - setResponse, - }), - ); - if (isPromise(result$)) { - return result$.then(() => response); - } - return response; -} diff --git a/packages/loaders/neo4j/src/index.ts b/packages/loaders/neo4j/src/index.ts index 218f0613fb794..3e5648d30a1c7 100644 --- a/packages/loaders/neo4j/src/index.ts +++ b/packages/loaders/neo4j/src/index.ts @@ -4,7 +4,10 @@ import { loadGraphQLSchemaFromNeo4J, LoadGraphQLSchemaFromNeo4JOpts } from './sc export function loadNeo4JSubgraph(name: string, opts: LoadGraphQLSchemaFromNeo4JOpts) { return ({ logger }: { logger: Logger }) => ({ name, - schema$: loadGraphQLSchemaFromNeo4J(name, opts), + schema$: loadGraphQLSchemaFromNeo4J(name, { + ...opts, + logger, + }), }); } diff --git a/packages/loaders/neo4j/src/schema.ts b/packages/loaders/neo4j/src/schema.ts index 480534ef9d32f..4754a5e823b57 100644 --- a/packages/loaders/neo4j/src/schema.ts +++ b/packages/loaders/neo4j/src/schema.ts @@ -111,7 +111,6 @@ export async function loadGraphQLSchemaFromNeo4J( const schema = await getExecutableSchemaFromTypeDefsAndDriver({ driver, - logger, pubsub, typeDefs, }); diff --git a/packages/loaders/soap/src/SOAPLoader.ts b/packages/loaders/soap/src/SOAPLoader.ts index cb7acefe8ff31..11a130d474f45 100644 --- a/packages/loaders/soap/src/SOAPLoader.ts +++ b/packages/loaders/soap/src/SOAPLoader.ts @@ -42,8 +42,8 @@ import { getInterpolatedHeadersFactory, ResolverDataBasedFactory, } from '@graphql-mesh/string-interpolation'; -import { MeshFetch } from '@graphql-mesh/types'; -import { sanitizeNameForGraphQL } from '@graphql-mesh/utils'; +import { Logger, MeshFetch } from '@graphql-mesh/types'; +import { DefaultLogger, sanitizeNameForGraphQL } from '@graphql-mesh/utils'; import { fetch as defaultFetchFn } from '@whatwg-node/fetch'; import { WSDLBinding, @@ -65,6 +65,7 @@ import { PARSE_XML_OPTIONS, SoapAnnotations } from './utils.js'; export interface SOAPLoaderOptions { subgraphName: string; fetch?: MeshFetch; + logger?: Logger; schemaHeaders?: Record; operationHeaders?: Record; } @@ -134,9 +135,11 @@ export class SOAPLoader { private schemaHeadersFactory: ResolverDataBasedFactory>; private fetchFn: MeshFetch; private subgraphName: string; + private logger: Logger; constructor(options: SOAPLoaderOptions) { this.fetchFn = options.fetch || defaultFetchFn; + this.logger = options.logger || new DefaultLogger(options.subgraphName); this.subgraphName = options.subgraphName; this.loadXMLSchemaNamespace(); this.schemaComposer.addDirective(soapDirective); @@ -754,9 +757,9 @@ export class SOAPLoader { }; } else { if (elementObj.attributes?.ref) { - console.warn(`element.ref isn't supported yet.`); + this.logger.warn(`element.ref isn't supported yet.`); } else { - console.warn(`Element doesn't have a name in ${complexTypeName}. Ignoring...`); + this.logger.warn(`Element doesn't have a name in ${complexTypeName}. Ignoring...`); } } } @@ -941,9 +944,9 @@ export class SOAPLoader { }; } else { if (elementObj.attributes?.ref) { - console.warn(`element.ref isn't supported yet.`, elementObj.attributes?.ref); + this.logger.warn(`element.ref isn't supported yet.`, elementObj.attributes?.ref); } else { - console.warn(`Element doesn't have a name in ${complexTypeName}. Ignoring...`); + this.logger.warn(`Element doesn't have a name in ${complexTypeName}. Ignoring...`); } } } diff --git a/packages/loaders/soap/src/index.ts b/packages/loaders/soap/src/index.ts index bff3c1f8084c5..57fa4cc4ec684 100644 --- a/packages/loaders/soap/src/index.ts +++ b/packages/loaders/soap/src/index.ts @@ -1,4 +1,4 @@ -import { MeshFetch } from '@graphql-mesh/types'; +import { Logger, MeshFetch } from '@graphql-mesh/types'; import { defaultImportFn, DefaultLogger, readFileOrUrl } from '@graphql-mesh/utils'; import { SOAPLoader } from './SOAPLoader.js'; @@ -9,15 +9,17 @@ export * from '@graphql-mesh/transport-soap'; export interface SOAPSubgraphLoaderOptions { source?: string; fetch?: MeshFetch; + logger?: Logger; schemaHeaders?: Record; operationHeaders?: Record; } export function loadSOAPSubgraph(subgraphName: string, options: SOAPSubgraphLoaderOptions) { - return ({ cwd, fetch }: { cwd: string; fetch: MeshFetch }) => { + return ({ cwd, fetch, logger }: { cwd: string; fetch: MeshFetch; logger: Logger }) => { const soapLoader = new SOAPLoader({ subgraphName, fetch: options.fetch || fetch, + logger: options.logger || logger, schemaHeaders: options.schemaHeaders, operationHeaders: options.operationHeaders, }); diff --git a/packages/loaders/soap/test/examples.test.ts b/packages/loaders/soap/test/examples.test.ts index f6ea9469fe437..f0b2364c4e881 100644 --- a/packages/loaders/soap/test/examples.test.ts +++ b/packages/loaders/soap/test/examples.test.ts @@ -2,6 +2,7 @@ import { promises } from 'fs'; import { join } from 'path'; import { printSchema } from 'graphql'; +import { Logger } from '@graphql-mesh/types'; import { fetch } from '@whatwg-node/fetch'; import { SOAPLoader } from '../src/index.js'; @@ -10,11 +11,20 @@ const { readFile } = promises; const examples = ['example1', 'example2', 'axis']; describe('Examples', () => { + const mockLogger: Logger = { + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + child: () => mockLogger, + }; examples.forEach(example => { it(`should generate schema for ${example}`, async () => { const soapLoader = new SOAPLoader({ subgraphName: example, fetch, + logger: mockLogger, }); const example1Wsdl = await readFile( join(__dirname, './fixtures/' + example + '.wsdl'), diff --git a/packages/loaders/soap/test/soap.test.ts b/packages/loaders/soap/test/soap.test.ts index a9bd89e0dcbd3..e5bf15c8b50d8 100644 --- a/packages/loaders/soap/test/soap.test.ts +++ b/packages/loaders/soap/test/soap.test.ts @@ -2,7 +2,7 @@ import { promises } from 'fs'; import { join } from 'path'; import { parse } from 'graphql'; -import { MeshFetch } from '@graphql-mesh/types'; +import { Logger, MeshFetch } from '@graphql-mesh/types'; import { printSchemaWithDirectives } from '@graphql-tools/utils'; import { fetch } from '@whatwg-node/fetch'; import { createExecutorFromSchemaAST, SOAPLoader } from '../src/index.js'; @@ -10,10 +10,19 @@ import { createExecutorFromSchemaAST, SOAPLoader } from '../src/index.js'; const { readFile } = promises; describe('SOAP Loader', () => { + const mockLogger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + log: jest.fn(), + child: () => mockLogger, + }; it('should generate the schema correctly', async () => { const soapLoader = new SOAPLoader({ subgraphName: 'Test', fetch, + logger: mockLogger, }); await soapLoader.fetchWSDL('https://www.w3schools.com/xml/tempconvert.asmx?WSDL'); const schema = soapLoader.buildSchema(); @@ -23,6 +32,7 @@ describe('SOAP Loader', () => { const soapLoader = new SOAPLoader({ subgraphName: 'Test', fetch, + logger: mockLogger, }); await soapLoader.fetchWSDL('https://www.crcind.com/csp/samples/SOAP.Demo.cls?WSDL'); const schema = soapLoader.buildSchema(); @@ -44,6 +54,7 @@ describe('SOAP Loader', () => { const soapLoader = new SOAPLoader({ subgraphName: 'Test', fetch, + logger: mockLogger, }); const example1Wsdl = await readFile(join(__dirname, './fixtures/greeting.wsdl'), 'utf8'); await soapLoader.loadWSDL(example1Wsdl); diff --git a/packages/plugins/hive/src/index.ts b/packages/plugins/hive/src/index.ts index 34e878a1acfc9..5fe9723b707a9 100644 --- a/packages/plugins/hive/src/index.ts +++ b/packages/plugins/hive/src/index.ts @@ -108,6 +108,8 @@ export default function useMeshHive( // Mesh already disposes the client below on Mesh's `destroy` event autoDispose: false, }); + // TODO: Remove later after v0 + // Pubsub.destroy will no longer function onTerminate() { return hiveClient .dispose() @@ -124,5 +126,8 @@ export default function useMeshHive( useYogaHive(hiveClient) as any, ); }, + [Symbol.asyncDispose]() { + return onTerminate(); + }, }; } diff --git a/packages/plugins/newrelic/src/index.ts b/packages/plugins/newrelic/src/index.ts index 35fafdce11b56..b7b4a0c8d091e 100644 --- a/packages/plugins/newrelic/src/index.ts +++ b/packages/plugins/newrelic/src/index.ts @@ -9,7 +9,7 @@ import { useNewRelic } from '@envelop/newrelic'; import { process } from '@graphql-mesh/cross-helpers'; import { stringInterpolator } from '@graphql-mesh/string-interpolation'; import { MeshPlugin, MeshPluginOptions, YamlConfig } from '@graphql-mesh/types'; -import { getHeadersObj } from '@graphql-mesh/utils'; +import { getHeadersObj, mapMaybePromise } from '@graphql-mesh/utils'; const DESTS = attributeFilter.DESTINATIONS; @@ -124,14 +124,10 @@ export default function useMeshNewrelic( } const res$ = requestHandler(...args); - if (res$) { - if (isPromise(res$)) { - return res$.then(sendResAttributes).then(() => res$); - } - sendResAttributes(res$); - } - - return res$; + return mapMaybePromise(res$, res => { + sendResAttributes(res); + return res; + }); }), ); } diff --git a/packages/plugins/prometheus/tests/prometheus.spec.ts b/packages/plugins/prometheus/tests/prometheus.spec.ts index 8f126e47c79fc..ed6dc2d7b6493 100644 --- a/packages/plugins/prometheus/tests/prometheus.spec.ts +++ b/packages/plugins/prometheus/tests/prometheus.spec.ts @@ -43,6 +43,7 @@ describe('Prometheus', () => { }; }, plugins: ctx => [usePrometheus(ctx)], + logging: false, }); } diff --git a/packages/serve-cli/package.json b/packages/serve-cli/package.json index 47b6ffc3bb19e..39e4a86f00aee 100644 --- a/packages/serve-cli/package.json +++ b/packages/serve-cli/package.json @@ -12,7 +12,7 @@ "node": ">=16.0.0" }, "bin": { - "mesh-serve": "dist/cjs/bin.js" + "mesh-serve": "dist/esm/bin.js" }, "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -54,11 +54,11 @@ "dotenv": "^16.3.1", "json-bigint-patch": "^0.0.8", "spinnies": "^0.5.1", - "tsx": "^4.7.1", - "uWebSockets.js": "uNetworking/uWebSockets.js#semver:^20" + "tsx": "^4.7.1" }, "optionalDependencies": { - "node-libcurl": "^4.0.0" + "node-libcurl": "^4.0.0", + "uWebSockets.js": "uNetworking/uWebSockets.js#semver:^20" }, "devDependencies": { "@parcel/watcher": "^2.3.0", diff --git a/packages/serve-cli/src/nodeHttp.ts b/packages/serve-cli/src/nodeHttp.ts index fb2dea9a6525e..9244c76329bcb 100644 --- a/packages/serve-cli/src/nodeHttp.ts +++ b/packages/serve-cli/src/nodeHttp.ts @@ -7,7 +7,6 @@ import { createServer as createHTTPSServer } from 'https'; // eslint-disable-next-line import/no-nodejs-modules import { SecureContextOptions } from 'tls'; import { RecognizedString } from 'uWebSockets.js'; -import { registerTerminateHandler } from '@graphql-mesh/utils'; import { ServerOptions } from './types.js'; export function readRecognizedString(recognizedString: RecognizedString) { @@ -27,7 +26,7 @@ export async function startNodeHttpServer({ host, port, sslCredentials, -}: ServerOptions): Promise { +}: ServerOptions): Promise { if (sslCredentials) { const sslOptionsForNodeHttp: SecureContextOptions = {}; if (sslCredentials.ca_file_name) { @@ -59,32 +58,42 @@ export async function startNodeHttpServer({ } const server = createHTTPSServer(sslOptionsForNodeHttp, handler); log.info(`Starting server on ${protocol}://${host}:${port}`); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, () => { log.info(`Server started on ${protocol}://${host}:${port}`); - registerTerminateHandler(eventName => { - log.info(`Closing server for ${eventName}`); - server.close(() => { - log.info(`Server closed for ${eventName}`); - resolve(); - }); + resolve({ + [Symbol.asyncDispose]() { + return new Promise(resolve => { + log.info(`Closing server`); + server.closeAllConnections(); + server.close(() => { + log.info(`Server closed`); + resolve(); + }); + }); + }, }); }); }); } const server = createHTTPServer(handler); log.info(`Starting server on ${protocol}://${host}:${port}`); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, () => { log.info(`Server started on ${protocol}://${host}:${port}`); - registerTerminateHandler(eventName => { - log.info(`Closing server for ${eventName}`); - server.close(() => { - log.info(`Server closed for ${eventName}`); - resolve(); - }); + resolve({ + [Symbol.asyncDispose]() { + return new Promise(resolve => { + log.info(`Closing server`); + server.closeAllConnections(); + server.close(() => { + log.info(`Server closed`); + resolve(); + }); + }); + }, }); }); }); diff --git a/packages/serve-cli/src/run.ts b/packages/serve-cli/src/run.ts index ff2ecfe374afd..0e1bf5e0c59c6 100644 --- a/packages/serve-cli/src/run.ts +++ b/packages/serve-cli/src/run.ts @@ -1,5 +1,6 @@ import 'json-bigint-patch'; // JSON.parse/stringify with bigints support -import 'tsx/cjs'; // support importing typescript configs +import 'tsx/cjs'; // support importing typescript configs in CommonJS +import 'tsx/esm'; // support importing typescript configs in ESM import 'dotenv/config'; // inject dotenv options to process.env // eslint-disable-next-line import/no-nodejs-modules @@ -11,7 +12,7 @@ import { dirname, isAbsolute, resolve } from 'path'; import { Command, InvalidArgumentError, Option } from '@commander-js/extra-typings'; import { createServeRuntime, UnifiedGraphConfig } from '@graphql-mesh/serve-runtime'; import { Logger } from '@graphql-mesh/types'; -import { DefaultLogger, registerTerminateHandler } from '@graphql-mesh/utils'; +import { DefaultLogger, getTerminateStack, registerTerminateHandler } from '@graphql-mesh/utils'; import { isValidPath } from '@graphql-tools/utils'; import { startNodeHttpServer } from './nodeHttp.js'; import { MeshServeCLIConfig } from './types.js'; @@ -79,6 +80,8 @@ export interface RunOptions extends ReturnType { version?: string; } +export type ImportedModule = T | { default: T }; + export async function run({ log: rootLog = new DefaultLogger(), productName = 'Mesh', @@ -98,33 +101,32 @@ export async function run({ ? opts.configPath : resolve(process.cwd(), opts.configPath); log.info(`Checking configuration at ${configPath}`); - const importedConfig: { serveConfig?: MeshServeCLIConfig } = await import(configPath).catch( - err => { - if (err.code === 'ERR_MODULE_NOT_FOUND') { - return {}; // no config is ok - } - log.error('Loading configuration failed!'); - throw err; - }, - ); - if (importedConfig.serveConfig) { + const importedConfigModule: ImportedModule<{ serveConfig?: MeshServeCLIConfig }> = await import( + configPath + ).catch(err => { + if (err.code === 'ERR_MODULE_NOT_FOUND') { + return {}; // no config is ok + } + log.error('Loading configuration failed!'); + throw err; + }); + let importedConfig: MeshServeCLIConfig; + if ('default' in importedConfigModule) { + log.info('Loaded configuration'); + importedConfig = importedConfigModule.default.serveConfig; + } else if ('serveConfig' in importedConfigModule) { log.info('Loaded configuration'); + importedConfig = importedConfigModule.serveConfig; } else { + importedConfig = {}; log.info('No configuration found'); } const config: MeshServeCLIConfig = { - ...importedConfig?.serveConfig, + ...importedConfig, ...opts, }; - if (config.pubsub) { - registerTerminateHandler(eventName => { - log.info(`Destroying pubsub for ${eventName}`); - config.pubsub!.publish('destroy', undefined); - }); - } - let unifiedGraphPath: UnifiedGraphConfig; let spec: 'federation' | 'fusion'; @@ -240,6 +242,8 @@ export async function run({ logging: log, ...config, }); + const terminateStack = getTerminateStack(); + terminateStack.use(handler); process.on('message', message => { if (message === 'invalidateUnifiedGraph') { log.info(`Invalidating ${unifiedGraphName}`); @@ -255,7 +259,7 @@ export async function run({ log.warn('uWebSockets.js is not available currently so the server will fallback to node:http.'); } const startServer = uWebSocketsAvailable ? startuWebSocketsServer : startNodeHttpServer; - await startServer({ + const server = await startServer({ handler, log, protocol, @@ -263,4 +267,5 @@ export async function run({ port, sslCredentials: config.sslCredentials, }); + terminateStack.use(server); } diff --git a/packages/serve-cli/src/uWebSockets.ts b/packages/serve-cli/src/uWebSockets.ts index 71c71082929cf..85ad4fcb0276a 100644 --- a/packages/serve-cli/src/uWebSockets.ts +++ b/packages/serve-cli/src/uWebSockets.ts @@ -1,4 +1,3 @@ -import { registerTerminateHandler } from '@graphql-mesh/utils'; import { ServerOptions } from './types.js'; export async function startuWebSocketsServer({ @@ -8,19 +7,20 @@ export async function startuWebSocketsServer({ host, port, sslCredentials, -}: ServerOptions): Promise { +}: ServerOptions): Promise { return import('uWebSockets.js').then(uWS => { const app = sslCredentials ? uWS.SSLApp(sslCredentials) : uWS.App(); app.any('/*', handler); log.info(`Starting server on ${protocol}://${host}:${port}`); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { app.listen(host, port, function listenCallback(listenSocket) { if (listenSocket) { - registerTerminateHandler(eventName => { - log.info(`Closing ${protocol}://${host}:${port} for ${eventName}`); - app.close(); + resolve({ + [Symbol.dispose]() { + log.info(`Closing ${protocol}://${host}:${port}`); + app.close(); + }, }); - resolve(); } else { reject(new Error(`Failed to start server on ${protocol}://${host}:${port}!`)); } diff --git a/packages/serve-runtime/package.json b/packages/serve-runtime/package.json index 1255ac551eec3..d4cdcf71d8787 100644 --- a/packages/serve-runtime/package.json +++ b/packages/serve-runtime/package.json @@ -47,6 +47,7 @@ "@graphql-tools/stitch": "^9.2.9", "@graphql-tools/utils": "^10.2.1", "@whatwg-node/server": "^0.9.34", + "disposablestack": "^1.1.6", "graphql-yoga": "^5.3.0" }, "publishConfig": { diff --git a/packages/serve-runtime/src/createServeRuntime.ts b/packages/serve-runtime/src/createServeRuntime.ts index 52b5c9e74345d..361d3e44f2a0a 100644 --- a/packages/serve-runtime/src/createServeRuntime.ts +++ b/packages/serve-runtime/src/createServeRuntime.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line import/no-nodejs-modules import type { IncomingMessage } from 'node:http'; +import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; import { GraphQLSchema, parse } from 'graphql'; import { createYoga, @@ -9,21 +10,32 @@ import { YogaServerInstance, type Plugin, } from 'graphql-yoga'; -import { useFusiongraph } from '@graphql-mesh/fusion-runtime'; +import { + handleFederationSupergraph, + handleFusiongraph, + isDisposable, + OnSubgraphExecuteHook, + UnifiedGraphHandler, + UnifiedGraphManager, +} from '@graphql-mesh/fusion-runtime'; // eslint-disable-next-line import/no-extraneous-dependencies -import { Logger, MeshFetch, OnFetchHook } from '@graphql-mesh/types'; -import { DefaultLogger, getHeadersObj, wrapFetchWithHooks } from '@graphql-mesh/utils'; +import { Logger, OnFetchHook } from '@graphql-mesh/types'; +import { + DefaultLogger, + getHeadersObj, + mapMaybePromise, + wrapFetchWithHooks, +} from '@graphql-mesh/utils'; import { useExecutor } from '@graphql-tools/executor-yoga'; -import { isPromise } from '@graphql-tools/utils'; +import { MaybePromise } from '@graphql-tools/utils'; import { getProxyExecutor } from './getProxyExecutor.js'; -import { handleUnifiedGraphConfig } from './handleUnifiedGraphConfig.js'; +import { handleUnifiedGraphConfig, UnifiedGraphConfig } from './handleUnifiedGraphConfig.js'; import { MeshServeConfig, MeshServeConfigContext, MeshServeContext, MeshServePlugin, } from './types.js'; -import { useFederationSupergraph } from './useFederationSupergraph.js'; export function createServeRuntime = Record>( config: MeshServeConfig, @@ -31,12 +43,11 @@ export function createServeRuntime = Record let fetchAPI: Partial = config.fetchAPI; // eslint-disable-next-line prefer-const let logger: Logger; - let wrappedFetchFn: MeshFetch; + const onFetchHooks: OnFetchHook[] = []; + const wrappedFetchFn = wrapFetchWithHooks(onFetchHooks); const configContext: MeshServeConfigContext = { - get fetch() { - return wrappedFetchFn; - }, + fetch: wrappedFetchFn, get logger() { return logger; }, @@ -45,97 +56,111 @@ export function createServeRuntime = Record pubsub: 'pubsub' in config ? config.pubsub : undefined, }; - let supergraphYogaPlugin: Plugin & { - invalidateUnifiedGraph: () => void; - }; + let unifiedGraphPlugin: Plugin; - if ('fusiongraph' in config) { - supergraphYogaPlugin = useFusiongraph({ - getFusiongraph: () => handleUnifiedGraphConfig(config.fusiongraph, configContext), - transports: config.transports, - polling: config.polling, - additionalResolvers: config.additionalResolvers, - transportBaseContext: configContext, - readinessCheckEndpoint: config.readinessCheckEndpoint || '/readiness', + const readinessCheckEndpoint = config.readinessCheckEndpoint || '/readiness'; + const onSubgraphExecuteHooks: OnSubgraphExecuteHook[] = []; + + let unifiedGraph: GraphQLSchema; + let schemaInvalidator: () => void; + let schemaFetcher: () => MaybePromise; + let contextBuilder: (context: T) => MaybePromise; + let readinessChecker: () => MaybePromise; + + const disposableStack = new AsyncDisposableStack(); + + if ('proxy' in config) { + const proxyExecutor = getProxyExecutor({ + config, + configContext, + getSchema: () => unifiedGraph, + onSubgraphExecuteHooks, + disposableStack, }); - } else if ('supergraph' in config) { - supergraphYogaPlugin = useFederationSupergraph({ - getFederationSupergraph: () => handleUnifiedGraphConfig(config.supergraph, configContext), + const executorPlugin = useExecutor(proxyExecutor); + unifiedGraphPlugin = executorPlugin; + readinessChecker = () => { + const res$ = proxyExecutor({ + document: parse(`query { __typename }`), + }); + return mapMaybePromise(res$, res => !isAsyncIterable(res) && !!res.data?.__typename); + }; + schemaInvalidator = () => executorPlugin.invalidateUnifiedGraph(); + } else { + let handleUnifiedGraph: UnifiedGraphHandler; + let unifiedGraphInConfig: UnifiedGraphConfig; + if ('fusiongraph' in config) { + handleUnifiedGraph = handleFusiongraph; + unifiedGraphInConfig = config.fusiongraph; + } else if ('supergraph' in config) { + handleUnifiedGraph = handleFederationSupergraph; + unifiedGraphInConfig = config.supergraph; + } + const unifiedGraphManager = new UnifiedGraphManager({ + getUnifiedGraph: () => handleUnifiedGraphConfig(unifiedGraphInConfig, configContext), + handleUnifiedGraph, transports: config.transports, polling: config.polling, additionalResolvers: config.additionalResolvers, transportBaseContext: configContext, - readinessCheckEndpoint: config.readinessCheckEndpoint || '/readiness', + readinessCheckEndpoint, + onDelegateHooks: [], + onSubgraphExecuteHooks, }); - } else if ('proxy' in config) { - let schema: GraphQLSchema; - const proxyExecutor = getProxyExecutor(config, configContext, schema); - // TODO: fix useExecutor typings to inherit the context - const executorPlugin = useExecutor(proxyExecutor) as unknown as Plugin< - MeshServeContext & TContext - > & { - invalidateSupergraph: () => void; - }; - supergraphYogaPlugin = { - onPluginInit({ addPlugin }) { - addPlugin(executorPlugin); - addPlugin( - // TODO: fix useReadinessCheck typings to inherit the context - useReadinessCheck({ - endpoint: config.readinessCheckEndpoint || '/readiness', - check() { - const res$ = proxyExecutor({ - document: parse(`query { __typename }`), - }); - if (isPromise(res$)) { - return res$.then( - res => !isAsyncIterable(res) && !!res.data?.__typename, - ) as Promise; - } - if (!isAsyncIterable(res$)) { - return !!res$.data?.__typename; - } - return false; - }, - }) as any, - ); - }, - onSchemaChange: payload => { - schema = payload.schema; - }, - invalidateUnifiedGraph: () => - (executorPlugin.invalidateSupergraph || (executorPlugin as any).invalidateUnifiedGraph)(), - }; + schemaFetcher = () => unifiedGraphManager.getUnifiedGraph(); + readinessChecker = () => + mapMaybePromise(unifiedGraphManager.getUnifiedGraph(), schema => !!schema); + schemaInvalidator = () => unifiedGraphManager.invalidateUnifiedGraph(); + contextBuilder = base => unifiedGraphManager.getContext(base); + disposableStack.use(unifiedGraphManager); } - const defaultFetchPlugin: MeshServePlugin = { + const readinessCheckPlugin = useReadinessCheck({ + endpoint: readinessCheckEndpoint, + // @ts-expect-error PromiseLike is not compatible with Promise + check: readinessChecker, + }); + + const defaultMeshPlugin: MeshServePlugin = { onFetch({ setFetchFn }) { setFetchFn(fetchAPI.fetch); }, - onYogaInit({ yoga }) { - const onFetchHooks: OnFetchHook[] = []; - - for (const plugin of yoga.getEnveloped._plugins as unknown as MeshServePlugin[]) { + onPluginInit({ plugins }) { + onFetchHooks.splice(0, onFetchHooks.length); + onSubgraphExecuteHooks.splice(0, onSubgraphExecuteHooks.length); + for (const plugin of plugins as MeshServePlugin[]) { if (plugin.onFetch) { onFetchHooks.push(plugin.onFetch); } + if (plugin.onSubgraphExecute) { + onSubgraphExecuteHooks.push(plugin.onSubgraphExecute); + } + if (isDisposable(plugin)) { + disposableStack.use(plugin); + } } - - wrappedFetchFn = wrapFetchWithHooks(onFetchHooks); }, }; const yoga = createYoga({ + // @ts-expect-error PromiseLike is not compatible with Promise + schema: schemaFetcher, fetchAPI: config.fetchAPI, logging: config.logging == null ? new DefaultLogger() : config.logging, - plugins: [defaultFetchPlugin, supergraphYogaPlugin, ...(config.plugins?.(configContext) || [])], - context: ({ request, params, ...rest }) => { + plugins: [ + defaultMeshPlugin, + unifiedGraphPlugin, + readinessCheckPlugin, + ...(config.plugins?.(configContext) || []), + ], + // @ts-expect-error PromiseLike is not compatible with Promise + context({ request, params, ...rest }) { // TODO: I dont like this cast, but it's necessary const { req, connectionParams } = rest as { req?: IncomingMessage; connectionParams?: Record; }; - return { + const baseContext = { ...configContext, request, params, @@ -150,6 +175,10 @@ export function createServeRuntime = Record {}, connectionParams, }; + if (contextBuilder) { + return contextBuilder(baseContext); + } + return baseContext; }, cors: config.cors, graphiql: config.graphiql, @@ -163,10 +192,18 @@ export function createServeRuntime = Record fetchAPI ||= yoga.fetchAPI; logger = yoga.logger as Logger; - Object.defineProperty(yoga, 'invalidateUnifiedGraph', { - value: supergraphYogaPlugin.invalidateUnifiedGraph, - configurable: true, + Object.defineProperties(yoga, { + invalidateUnifiedGraph: { + value: schemaInvalidator, + configurable: true, + }, + [Symbol.asyncDispose]: { + value: () => disposableStack.disposeAsync(), + configurable: true, + }, }); - return yoga as YogaServerInstance & { invalidateUnifiedGraph(): void }; + return yoga as YogaServerInstance & { + invalidateUnifiedGraph(): void; + } & AsyncDisposable; } diff --git a/packages/serve-runtime/src/getProxyExecutor.ts b/packages/serve-runtime/src/getProxyExecutor.ts index 3f2c646517696..6c2ae4dc3ff9c 100644 --- a/packages/serve-runtime/src/getProxyExecutor.ts +++ b/packages/serve-runtime/src/getProxyExecutor.ts @@ -1,31 +1,29 @@ import { GraphQLSchema } from 'graphql'; import { - defaultTransportsOption, getOnSubgraphExecute, + OnSubgraphExecuteHook, TransportEntry, } from '@graphql-mesh/fusion-runtime'; import { Executor } from '@graphql-tools/utils'; import { MeshServeConfigContext, MeshServeConfigWithProxy } from './types.js'; -export function getProxyExecutor( - config: MeshServeConfigWithProxy, - configContext: MeshServeConfigContext, - schema?: GraphQLSchema, -): Executor { +export function getProxyExecutor({ + config, + configContext, + getSchema, + onSubgraphExecuteHooks, + disposableStack, +}: { + config: MeshServeConfigWithProxy; + configContext: MeshServeConfigContext; + getSchema: () => GraphQLSchema; + onSubgraphExecuteHooks: OnSubgraphExecuteHook[]; + disposableStack: AsyncDisposableStack; +}): Executor { const fakeTransportEntryMap: Record = {}; let subgraphName: string = 'upstream'; const onSubgraphExecute = getOnSubgraphExecute({ - plugins: config.plugins?.(configContext as any) as any, - fusiongraph: schema, - transports() { - if (typeof config.transport === 'object') { - return config.transport; - } - if (typeof config.transport === 'function') { - return config.transport() as any; - } - return defaultTransportsOption('http'); - }, + onSubgraphExecuteHooks, transportEntryMap: new Proxy(fakeTransportEntryMap, { get(fakeTransportEntryMap, subgraphNameProp: string): TransportEntry { if (!fakeTransportEntryMap[subgraphNameProp]) { @@ -42,13 +40,8 @@ export function getProxyExecutor( }, }), transportBaseContext: configContext, - subgraphMap: new Proxy(new Map(), { - get(_, subgraphNameProp: string) { - if (subgraphNameProp === 'get') { - return () => schema; - } - }, - }), + getSubgraphSchema: getSchema, + disposableStack, }); return function proxyExecutor(executionRequest) { return onSubgraphExecute(subgraphName, executionRequest); diff --git a/packages/serve-runtime/src/handleUnifiedGraphConfig.ts b/packages/serve-runtime/src/handleUnifiedGraphConfig.ts index b5d22bff7be26..3ba7f91826def 100644 --- a/packages/serve-runtime/src/handleUnifiedGraphConfig.ts +++ b/packages/serve-runtime/src/handleUnifiedGraphConfig.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import { buildASTSchema, buildSchema, DocumentNode, GraphQLSchema, isSchema } from 'graphql'; -import { defaultImportFn, isUrl, readFileOrUrl } from '@graphql-mesh/utils'; -import { isDocumentNode, isPromise, isValidPath } from '@graphql-tools/utils'; +import { defaultImportFn, isUrl, mapMaybePromise, readFileOrUrl } from '@graphql-mesh/utils'; +import { isDocumentNode, isValidPath, MaybePromise } from '@graphql-tools/utils'; import { MeshServeConfigContext } from './types.js'; export type UnifiedGraphSchema = GraphQLSchema | DocumentNode | string; @@ -14,12 +14,10 @@ export type UnifiedGraphConfig = export function handleUnifiedGraphConfig( config: UnifiedGraphConfig, configContext: MeshServeConfigContext, -): Promise | GraphQLSchema { - const config$ = typeof config === 'function' ? config() : config; - if (isPromise(config$)) { - return config$.then(schema => handleUnifiedGraphSchema(schema, configContext)); - } - return handleUnifiedGraphSchema(config$, configContext); +): MaybePromise { + return mapMaybePromise(typeof config === 'function' ? config() : config, schema => + handleUnifiedGraphSchema(schema, configContext), + ); } export function handleUnifiedGraphSchema( diff --git a/packages/serve-runtime/src/types.ts b/packages/serve-runtime/src/types.ts index 4d40908f9c44d..85a398ebad414 100644 --- a/packages/serve-runtime/src/types.ts +++ b/packages/serve-runtime/src/types.ts @@ -9,7 +9,7 @@ import { YogaServerOptions, } from 'graphql-yoga'; import { Plugin as EnvelopPlugin } from '@envelop/core'; -import { FusiongraphPlugin, Transport, TransportsOption } from '@graphql-mesh/fusion-runtime'; +import { Transport, TransportsOption, UnifiedGraphPlugin } from '@graphql-mesh/fusion-runtime'; // eslint-disable-next-line import/no-extraneous-dependencies import { KeyValueCache, Logger, MeshFetch, MeshPubSub, OnFetchHook } from '@graphql-mesh/types'; import { HTTPExecutorOptions } from '@graphql-tools/executor-http'; @@ -59,15 +59,15 @@ export type MeshServePlugin< TPluginContext extends Record = Record, TContext extends Record = Record, > = YogaPlugin & MeshServeContext & TContext> & - FusiongraphPlugin & { + UnifiedGraphPlugin & { onFetch?: OnFetchHook & MeshServeContext & TContext>; - }; + } & Partial; interface MeshServeConfigWithFusiongraph extends MeshServeConfigWithoutSource { /** * Path to the GraphQL Fusion unified schema. */ - fusiongraph: UnifiedGraphConfig; + fusiongraph?: UnifiedGraphConfig; /** * Polling interval in milliseconds. */ @@ -88,7 +88,7 @@ interface MeshServeConfigWithSupergraph extends MeshServeConfigWithout /** * Path to the Apollo Federation unified schema. */ - supergraph: UnifiedGraphConfig; + supergraph?: UnifiedGraphConfig; /** * Polling interval in milliseconds. */ diff --git a/packages/serve-runtime/src/useFederationSupergraph.ts b/packages/serve-runtime/src/useFederationSupergraph.ts deleted file mode 100644 index 2c41b0c2a2dfd..0000000000000 --- a/packages/serve-runtime/src/useFederationSupergraph.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { DocumentNode, GraphQLSchema, isSchema, parse } from 'graphql'; -import { Plugin, PromiseOrValue, useReadinessCheck, YogaServer } from 'graphql-yoga'; -import { - defaultTransportsOption, - FusiongraphPlugin, - getOnSubgraphExecute, - TransportsOption, -} from '@graphql-mesh/fusion-runtime'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { getInContextSDK } from '@graphql-mesh/runtime'; -import { TransportBaseContext, TransportEntry } from '@graphql-mesh/transport-common'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { OnDelegateHook } from '@graphql-mesh/types'; -import { mapMaybePromise } from '@graphql-mesh/utils'; -import { - filterInternalFieldsAndTypes, - getStitchingOptionsFromSupergraphSdl, -} from '@graphql-tools/federation'; -import { stitchSchemas } from '@graphql-tools/stitch'; -import { getDocumentNodeFromSchema, IResolvers, isPromise } from '@graphql-tools/utils'; - -export interface FederationSupergraphPluginOptions { - getFederationSupergraph( - baseCtx: TransportBaseContext, - ): GraphQLSchema | DocumentNode | string | Promise; - transports?: TransportsOption; - polling?: number; - additionalTypedefs?: DocumentNode | string | DocumentNode[] | string[]; - additionalResolvers?: IResolvers | IResolvers[]; - transportBaseContext?: TransportBaseContext; - readinessCheckEndpoint?: string; -} - -function ensureSchemaAST(source: GraphQLSchema | DocumentNode | string) { - if (isSchema(source)) { - return getDocumentNodeFromSchema(source); - } - if (typeof source === 'string') { - return parse(source, { noLocation: true }); - } - return source; -} - -export function useFederationSupergraph = Record>( - opts: FederationSupergraphPluginOptions, -): Plugin & { - invalidateUnifiedGraph(): void; -} { - let supergraph: GraphQLSchema; - let lastLoadedSupergraph: string | GraphQLSchema | DocumentNode; - let yoga: YogaServer; - // TODO: We need to figure this out in a better way - let inContextSDK: any; - function handleLoadedSupergraph(loadedSupergraph: string | GraphQLSchema | DocumentNode) { - if (loadedSupergraph != null && lastLoadedSupergraph === loadedSupergraph) { - return; - } - lastLoadedSupergraph = loadedSupergraph; - const schemaAST = ensureSchemaAST(loadedSupergraph); - const transportEntryMap: Record = {}; - const { subschemas, typeDefs, typeMergingOptions } = getStitchingOptionsFromSupergraphSdl({ - supergraphSdl: schemaAST, - batch: true, - onSubschemaConfig(subschemaConfig) { - transportEntryMap[subschemaConfig.name] = { - subgraph: subschemaConfig.name, - kind: 'http', - location: subschemaConfig.endpoint, - }; - subschemaConfig.executor = function executor(execReq) { - return onSubgraphExecute(subschemaConfig.name, execReq); - }; - }, - }); - const subgraphMap: Map = new Map(); - const onSubgraphExecute = getOnSubgraphExecute({ - fusiongraph: supergraph, - plugins: yoga.getEnveloped._plugins as FusiongraphPlugin[], - transports: opts.transports || defaultTransportsOption, - transportBaseContext: opts.transportBaseContext, - transportEntryMap, - subgraphMap, - }); - for (const subschemaConfig of subschemas) { - subgraphMap.set(subschemaConfig.name!, subschemaConfig.schema); - } - supergraph = stitchSchemas({ - subschemas, - assumeValid: true, - assumeValidSDL: true, - typeDefs: [typeDefs, opts.additionalTypedefs], - resolvers: opts.additionalResolvers as any, - typeMergingOptions, - }); - supergraph = filterInternalFieldsAndTypes(supergraph); - if (opts.additionalResolvers) { - const onDelegateHooks: OnDelegateHook[] = []; - for (const plugin of yoga.getEnveloped._plugins as any[]) { - if (plugin.onDelegate) { - onDelegateHooks.push(plugin.onDelegate); - } - } - inContextSDK = getInContextSDK( - supergraph, - subschemas as any[], - opts.transportBaseContext?.logger, - onDelegateHooks, - ); - } - } - function getAndSetSupergraph(): PromiseOrValue { - const supergraph$ = opts.getFederationSupergraph(opts.transportBaseContext); - return mapMaybePromise(supergraph$, handleLoadedSupergraph) as PromiseOrValue; - } - if (opts.polling) { - setInterval(getAndSetSupergraph, opts.polling); - } - let initialSupergraph$: PromiseOrValue; - let initiated = false; - return { - onYogaInit(payload) { - yoga = payload.yoga; - }, - onPluginInit({ addPlugin }) { - if (opts.readinessCheckEndpoint) { - addPlugin( - // TODO: fix useReadinessCheck typings to inherit the context - useReadinessCheck({ - endpoint: opts.readinessCheckEndpoint, - check() { - if (!initiated) { - initialSupergraph$ = getAndSetSupergraph(); - } - initiated = true; - if (isPromise(initialSupergraph$)) { - return initialSupergraph$.then(() => !!supergraph); - } - return !!supergraph; - }, - }) as any, - ); - } - }, - onRequestParse() { - return { - onRequestParseDone() { - if (!initiated) { - initialSupergraph$ = getAndSetSupergraph(); - } - initiated = true; - return initialSupergraph$; - }, - }; - }, - onEnveloped({ setSchema }: { setSchema: (schema: GraphQLSchema) => void }) { - setSchema(supergraph); - }, - onContextBuilding({ extendContext }) { - if (inContextSDK) { - extendContext(inContextSDK); - } - extendContext(opts.transportBaseContext as any); - }, - invalidateUnifiedGraph() { - return getAndSetSupergraph(); - }, - }; -} diff --git a/packages/serve-runtime/tests/serve-runtime.spec.ts b/packages/serve-runtime/tests/serve-runtime.spec.ts index aad8706181c2f..b63dcbd56a44d 100644 --- a/packages/serve-runtime/tests/serve-runtime.spec.ts +++ b/packages/serve-runtime/tests/serve-runtime.spec.ts @@ -21,6 +21,7 @@ describe('Serve Runtime', () => { }); const upstreamAPI = createYoga({ schema: upstreamSchema, + logging: false, }); let upstreamIsUp = true; const serveRuntimes = { diff --git a/packages/serve-runtime/tests/useForwardHeaders.spec.ts b/packages/serve-runtime/tests/useForwardHeaders.spec.ts index b745b14f3160e..34f57f7217f81 100644 --- a/packages/serve-runtime/tests/useForwardHeaders.spec.ts +++ b/packages/serve-runtime/tests/useForwardHeaders.spec.ts @@ -20,6 +20,7 @@ describe('useForwardHeaders', () => { }, }), plugins: [requestTrackerPlugin], + logging: false, }); beforeEach(() => { requestTrackerPlugin.onParams.mockClear(); diff --git a/packages/transports/common/src/types.ts b/packages/transports/common/src/types.ts index f4fc1cee31f78..7a4b2596509bd 100644 --- a/packages/transports/common/src/types.ts +++ b/packages/transports/common/src/types.ts @@ -35,3 +35,5 @@ export type TransportExecutorFactoryFn< export type Transport = { getSubgraphExecutor?: TransportExecutorFactoryFn; }; + +export type DisposableExecutor = Executor & Partial; diff --git a/packages/transports/mysql/src/execution.ts b/packages/transports/mysql/src/execution.ts index eaaabe8576c9f..471a8a3de1fce 100644 --- a/packages/transports/mysql/src/execution.ts +++ b/packages/transports/mysql/src/execution.ts @@ -3,10 +3,11 @@ import graphqlFields from 'graphql-fields'; import { createPool, PoolConnection, type Pool } from 'mysql'; import { introspection, upgrade } from 'mysql-utilities'; import { util } from '@graphql-mesh/cross-helpers'; -import { Logger, MeshPubSub } from '@graphql-mesh/types'; +import { DisposableExecutor } from '@graphql-mesh/transport-common'; +import { Logger } from '@graphql-mesh/types'; import { getDefDirectives } from '@graphql-mesh/utils'; import { createDefaultExecutor } from '@graphql-tools/delegate'; -import { Executor, getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'; import { getConnectionOptsFromEndpointUri } from './parseEndpointUri.js'; import { MySQLContext } from './types.js'; @@ -20,16 +21,10 @@ function getFieldsFromResolveInfo(info: GraphQLResolveInfo) { export interface GetMySQLExecutorOpts { subgraph: GraphQLSchema; pool?: Pool; - pubsub?: MeshPubSub; - logger: Logger; } -export function getMySQLExecutor({ - subgraph, - pool, - pubsub, - logger, -}: GetMySQLExecutorOpts): Executor { +export function getMySQLExecutor({ subgraph, pool }: GetMySQLExecutorOpts): DisposableExecutor { + const mysqlConnectionByContext = new WeakMap(); subgraph = mapSchema(subgraph, { [MapperKind.OBJECT_FIELD](fieldConfig, fieldName) { const directives = getDefDirectives(subgraph, fieldConfig); @@ -193,21 +188,8 @@ export function getMySQLExecutor({ const defaultExecutor = createDefaultExecutor(subgraph); const getConnection$ = util.promisify(pool.getConnection.bind(pool)); - if (pubsub) { - const id = pubsub.subscribe('destroy', () => { - pool.end(err => { - if (err) { - console.error(err); - } - pubsub.unsubscribe(id); - }); - }); - } else { - logger?.warn( - `FIXME: No pubsub provided for mysql executor, so the connection pool will never be closed`, - ); - } - return async function mysqlExecutor(executionRequest) { + + const executor: DisposableExecutor = async function mysqlExecutor(executionRequest) { const mysqlConnection = await getConnection$(); mysqlConnectionByContext.set(executionRequest.context, mysqlConnection); try { @@ -217,6 +199,16 @@ export function getMySQLExecutor({ mysqlConnection.release(); } }; + executor[Symbol.asyncDispose] = function dispose() { + return new Promise((resolve, reject) => { + pool.end(err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }; + return executor; } - -const mysqlConnectionByContext = new WeakMap(); diff --git a/packages/transports/mysql/src/index.ts b/packages/transports/mysql/src/index.ts index caf81258897f4..9550f57b181c2 100644 --- a/packages/transports/mysql/src/index.ts +++ b/packages/transports/mysql/src/index.ts @@ -6,10 +6,8 @@ export * from './execution.js'; export * from './parseEndpointUri.js'; export const getSubgraphExecutor: TransportExecutorFactoryFn<'mysql', never> = - function getMySQLSubgraphExecutor({ subgraph, pubsub, logger }) { + function getMySQLSubgraphExecutor({ subgraph }) { return getMySQLExecutor({ subgraph, - pubsub, - logger, }); }; diff --git a/packages/transports/neo4j/src/executor.ts b/packages/transports/neo4j/src/executor.ts index 06925b02180c0..2624a7f6c96a5 100644 --- a/packages/transports/neo4j/src/executor.ts +++ b/packages/transports/neo4j/src/executor.ts @@ -1,9 +1,10 @@ import { DefinitionNode, DirectiveNode, DocumentNode, GraphQLSchema, parse, visit } from 'graphql'; import { GraphQLBigInt } from 'graphql-scalars'; import { Driver } from 'neo4j-driver'; +import { DisposableExecutor } from '@graphql-mesh/transport-common'; import { Logger, MeshPubSub } from '@graphql-mesh/types'; import { createDefaultExecutor } from '@graphql-tools/delegate'; -import { Executor, getDirective, getDocumentNodeFromSchema } from '@graphql-tools/utils'; +import { getDirective, getDocumentNodeFromSchema } from '@graphql-tools/utils'; import { Neo4jGraphQL } from '@neo4j/graphql'; import { getDriverFromOpts } from './driver.js'; import { getEventEmitterFromPubSub } from './eventEmitterForPubSub.js'; @@ -28,7 +29,7 @@ function filterIntrospectionDefinitions { +export async function getNeo4JExecutor(opts: Neo4JExecutorOpts): Promise { const transportDirectives = getDirective(opts.schema, opts.schema, 'transport'); if (!transportDirectives?.length) { throw new Error('No transport directive found on the schema!'); @@ -72,7 +73,6 @@ export async function getNeo4JExecutor(opts: Neo4JExecutorOpts): Promise {}, }, }; - const id = pubsub.subscribe('destroy', async () => { - pubsub.unsubscribe(id); - logger?.debug('Closing Neo4j'); - await driver.close(); - logger?.debug('Neo4j closed'); - }); - } else { - logger?.warn( - 'FIXME: No pubsub provided for neo4j executor, so the connection will never be closed', - ); } const neo4jGraphQL = new Neo4jGraphQL({ typeDefs, diff --git a/packages/transports/rest/src/directives/httpOperation.ts b/packages/transports/rest/src/directives/httpOperation.ts index 2db3d1c9abe66..c2a4201485308 100644 --- a/packages/transports/rest/src/directives/httpOperation.ts +++ b/packages/transports/rest/src/directives/httpOperation.ts @@ -137,13 +137,7 @@ export function addHTTPRootFieldResolver( const binaryUpload = await args.input; if (isFileUpload(binaryUpload)) { const readable = binaryUpload.createReadStream(); - const chunks: number[] = []; - for await (const chunk of readable) { - for (const byte of chunk) { - chunks.push(byte); - } - } - requestInit.body = new Uint8Array(chunks); + requestInit.body = readable as ReadableStream; const [, contentType] = Object.entries(headers).find(([key]) => key.toLowerCase() === 'content-type') || []; diff --git a/setup-jest.js b/setup-jest.js new file mode 100644 index 0000000000000..7f31939051b80 --- /dev/null +++ b/setup-jest.js @@ -0,0 +1,3 @@ +Symbol.dispose ||= Symbol.for('Symbol.dispose'); +Symbol.asyncDispose ||= Symbol.for('Symbol.asyncDispose'); +require('json-bigint-patch'); diff --git a/tsconfig.json b/tsconfig.json index 119adb8fc19c1..074e5355c59db 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - // part of @tsconfig/node16 - "lib": ["es2021"], + "lib": ["ESNext"], // "outDir": "dist", diff --git a/yarn.lock b/yarn.lock index 89fdb67ce654b..71f29a580602e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1628,6 +1628,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-proposal-explicit-resource-management@npm:7.24.7": + version: 7.24.7 + resolution: "@babel/plugin-proposal-explicit-resource-management@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/plugin-syntax-explicit-resource-management": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/c07b2f44763b381f83cd36a70be16be72d10357a6ab986203a0af795bcf4bf917f0f3b8356b8bee30cfacdba410a8c8186e98ce6828c46e6315c56873ba9971d + languageName: node + linkType: hard + "@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.16.0": version: 7.18.6 resolution: "@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6" @@ -1781,6 +1793,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-explicit-resource-management@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/plugin-syntax-explicit-resource-management@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/4a4bc77a4a4fa69a29871d470604d69d3eb451153a4eda3bc956ae28dccd2d251ad4f2cc62076f326260314b873ef72230ec985eec416f8f7ebb81df0402636a + languageName: node + linkType: hard + "@babel/plugin-syntax-export-namespace-from@npm:^7.8.3": version: 7.8.3 resolution: "@babel/plugin-syntax-export-namespace-from@npm:7.8.3" @@ -3725,15 +3748,6 @@ __metadata: languageName: unknown linkType: soft -"@e2e/pubsub-destroy@workspace:e2e/pubsub-destroy": - version: 0.0.0-use.local - resolution: "@e2e/pubsub-destroy@workspace:e2e/pubsub-destroy" - dependencies: - "@graphql-mesh/serve-cli": "workspace:*" - "@graphql-mesh/utils": "workspace:*" - languageName: unknown - linkType: soft - "@e2e/soap-demo@workspace:e2e/soap-demo": version: 0.0.0-use.local resolution: "@e2e/soap-demo@workspace:e2e/soap-demo" @@ -4766,7 +4780,7 @@ __metadata: "@graphql-mesh/types": ^0.98.7 graphql: "*" bin: - mesh-compose: dist/cjs/bin.js + mesh-compose: dist/esm/bin.js languageName: unknown linkType: soft @@ -4868,10 +4882,12 @@ __metadata: "@graphql-mesh/utils": "npm:^0.98.7" "@graphql-tools/delegate": "npm:^10.0.11" "@graphql-tools/executor": "npm:^1.2.6" + "@graphql-tools/federation": "npm:^2.0.0" "@graphql-tools/stitch": "npm:^9.2.9" "@graphql-tools/stitching-directives": "npm:^3.0.2" "@graphql-tools/utils": "npm:^10.2.1" "@graphql-tools/wrap": "npm:^10.0.5" + disposablestack: "npm:^1.1.6" graphql-yoga: "npm:^5.3.0" tslib: "npm:^2.4.0" peerDependencies: @@ -5416,11 +5432,13 @@ __metadata: dependenciesMeta: node-libcurl: optional: true + uWebSockets.js: + optional: true peerDependenciesMeta: "@parcel/watcher": optional: true bin: - mesh-serve: dist/cjs/bin.js + mesh-serve: dist/esm/bin.js languageName: unknown linkType: soft @@ -5440,6 +5458,7 @@ __metadata: "@graphql-tools/stitch": "npm:^9.2.9" "@graphql-tools/utils": "npm:^10.2.1" "@whatwg-node/server": "npm:^0.9.34" + disposablestack: "npm:^1.1.6" graphql-yoga: "npm:^5.3.0" peerDependencies: graphql: "*" @@ -5979,6 +5998,7 @@ __metadata: "@types/lodash.topath": "npm:4.5.9" "@types/object-hash": "npm:3.0.6" "@whatwg-node/fetch": "npm:^0.9.13" + disposablestack: "npm:^1.1.6" dset: "npm:^3.1.2" js-yaml: "npm:^4.1.0" lodash.get: "npm:^4.4.2" @@ -6159,8 +6179,8 @@ __metadata: linkType: hard "@graphql-tools/federation@npm:^2.0.0": - version: 2.0.1 - resolution: "@graphql-tools/federation@npm:2.0.1" + version: 2.0.0 + resolution: "@graphql-tools/federation@npm:2.0.0" dependencies: "@apollo/client": "npm:~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0 || ~3.8.0 || ~3.9.0 || ~3.10.0" "@graphql-tools/delegate": "npm:^10.0.11" @@ -6168,7 +6188,7 @@ __metadata: "@graphql-tools/merge": "npm:^9.0.3" "@graphql-tools/schema": "npm:^10.0.4" "@graphql-tools/stitch": "npm:^9.2.9" - "@graphql-tools/utils": "npm:^10.2.2" + "@graphql-tools/utils": "npm:^10.2.1" "@graphql-tools/wrap": "npm:^10.0.3" "@whatwg-node/fetch": "npm:^0.9.17" tslib: "npm:^2.4.0" @@ -6178,7 +6198,7 @@ __metadata: dependenciesMeta: "@apollo/client": optional: true - checksum: 10c0/e34752a8907807ccefa99ccea55690776e083eb8dcb3cdb7f5dcfb4a9da885cda8cc800e928d359518bfe98d8de8bb3f10dc5980bcb41e3642234569cf9946ad + checksum: 10c0/1097e1a43f3e0082d1a5081eedc6f4a6992b7f4dc7af5d9da243b3f99008aa87624e111a1c5d27e20fe2c9a5619431ebb58d200b54f73fdddd6810f3cc5c3fe1 languageName: node linkType: hard @@ -6594,7 +6614,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/utils@npm:10.2.2, @graphql-tools/utils@npm:^10.0.0, @graphql-tools/utils@npm:^10.0.13, @graphql-tools/utils@npm:^10.0.3, @graphql-tools/utils@npm:^10.1.0, @graphql-tools/utils@npm:^10.1.1, @graphql-tools/utils@npm:^10.2.1, @graphql-tools/utils@npm:^10.2.2": +"@graphql-tools/utils@npm:10.2.2, @graphql-tools/utils@npm:^10.0.0, @graphql-tools/utils@npm:^10.0.13, @graphql-tools/utils@npm:^10.0.3, @graphql-tools/utils@npm:^10.1.0, @graphql-tools/utils@npm:^10.1.1, @graphql-tools/utils@npm:^10.2.1": version: 10.2.2 resolution: "@graphql-tools/utils@npm:10.2.2" dependencies: @@ -15959,7 +15979,7 @@ __metadata: languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" dependencies: @@ -16208,6 +16228,25 @@ __metadata: languageName: node linkType: hard +"disposablestack@npm:^1.1.6": + version: 1.1.6 + resolution: "disposablestack@npm:1.1.6" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.3" + es-errors: "npm:^1.3.0" + es-set-tostringtag: "npm:^2.0.3" + get-intrinsic: "npm:^1.2.4" + globalthis: "npm:^1.0.4" + has-symbols: "npm:^1.0.3" + hasown: "npm:^2.0.2" + internal-slot: "npm:^1.0.7" + suppressed-error: "npm:^1.0.3" + checksum: 10c0/ea7e165148f076f3c9e7f40aa8f8deb7d33cd9d09c3938c593a899bdbc43fd1cd746182c0cefb0c54be6ec375a5634e68907408f93a2916bb50c6861f978ae62 + languageName: node + linkType: hard + "dlv@npm:^1.1.3": version: 1.1.3 resolution: "dlv@npm:1.1.3" @@ -16758,7 +16797,7 @@ __metadata: languageName: node linkType: hard -"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": +"es-errors@npm:^1.1.0, es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 @@ -19271,7 +19310,7 @@ __metadata: languageName: node linkType: hard -"globalthis@npm:^1.0.3": +"globalthis@npm:^1.0.3, globalthis@npm:^1.0.4": version: 1.0.4 resolution: "globalthis@npm:1.0.4" dependencies: @@ -19573,6 +19612,7 @@ __metadata: "@ardatan/graphql-to-config-schema": "npm:0.1.25" "@babel/core": "npm:7.24.7" "@babel/plugin-proposal-class-properties": "npm:7.18.6" + "@babel/plugin-proposal-explicit-resource-management": "npm:7.24.7" "@babel/preset-env": "npm:7.24.7" "@babel/preset-typescript": "npm:7.24.7" "@changesets/changelog-github": "npm:0.5.0" @@ -19851,7 +19891,7 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.1, has-property-descriptors@npm:^1.0.2": version: 1.0.2 resolution: "has-property-descriptors@npm:1.0.2" dependencies: @@ -32493,6 +32533,22 @@ __metadata: languageName: node linkType: hard +"suppressed-error@npm:^1.0.3": + version: 1.0.3 + resolution: "suppressed-error@npm:1.0.3" + dependencies: + define-data-property: "npm:^1.1.1" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.22.3" + es-errors: "npm:^1.1.0" + function-bind: "npm:^1.1.2" + globalthis: "npm:^1.0.3" + has-property-descriptors: "npm:^1.0.1" + set-function-name: "npm:^2.0.1" + checksum: 10c0/e16592004cabda11bc257d437264d657ed2fabf20b6bea0f2b1978fcb6e0c20b09e398c92773c337e21ef856bb247e320e907c734ccd1cae96395823e6d84aef + languageName: node + linkType: hard + "svg-parser@npm:^2.0.2": version: 2.0.4 resolution: "svg-parser@npm:2.0.4"