diff --git a/.changeset/@envelop_apollo-tracing-1487-dependencies.md b/.changeset/@envelop_apollo-tracing-1487-dependencies.md new file mode 100644 index 0000000000..5521db1409 --- /dev/null +++ b/.changeset/@envelop_apollo-tracing-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/apollo-tracing': patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_auth0-1487-dependencies.md b/.changeset/@envelop_auth0-1487-dependencies.md new file mode 100644 index 0000000000..7c81732649 --- /dev/null +++ b/.changeset/@envelop_auth0-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/auth0': patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_core-1487-dependencies.md b/.changeset/@envelop_core-1487-dependencies.md new file mode 100644 index 0000000000..38b760f7fc --- /dev/null +++ b/.changeset/@envelop_core-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/core': patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_dataloader-1487-dependencies.md b/.changeset/@envelop_dataloader-1487-dependencies.md new file mode 100644 index 0000000000..98d6d52b4e --- /dev/null +++ b/.changeset/@envelop_dataloader-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/dataloader': patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_opentelemetry-1487-dependencies.md b/.changeset/@envelop_opentelemetry-1487-dependencies.md new file mode 100644 index 0000000000..feba642c7a --- /dev/null +++ b/.changeset/@envelop_opentelemetry-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/opentelemetry': patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_preload-assets-1487-dependencies.md b/.changeset/@envelop_preload-assets-1487-dependencies.md new file mode 100644 index 0000000000..c457eb43ec --- /dev/null +++ b/.changeset/@envelop_preload-assets-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/preload-assets': patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_prometheus-1487-dependencies.md b/.changeset/@envelop_prometheus-1487-dependencies.md new file mode 100644 index 0000000000..26f1d6717e --- /dev/null +++ b/.changeset/@envelop_prometheus-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/prometheus': patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_rate-limiter-1487-dependencies.md b/.changeset/@envelop_rate-limiter-1487-dependencies.md new file mode 100644 index 0000000000..2e99c1285a --- /dev/null +++ b/.changeset/@envelop_rate-limiter-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/rate-limiter': patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_statsd-1487-dependencies.md b/.changeset/@envelop_statsd-1487-dependencies.md new file mode 100644 index 0000000000..15f3d6bc8c --- /dev/null +++ b/.changeset/@envelop_statsd-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/statsd': patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_types-1487-dependencies.md b/.changeset/@envelop_types-1487-dependencies.md new file mode 100644 index 0000000000..9e8a541b0e --- /dev/null +++ b/.changeset/@envelop_types-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +'@envelop/types': patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/curvy-bottles-repeat.md b/.changeset/curvy-bottles-repeat.md new file mode 100644 index 0000000000..1514400798 --- /dev/null +++ b/.changeset/curvy-bottles-repeat.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': major +--- + +Remove async schema loading plugin. This was a mistake from beginning as we cannot asynchronously `validate` and `parse` since with GraphQL.js are synchronous in nature. diff --git a/.changeset/four-masks-jam.md b/.changeset/four-masks-jam.md new file mode 100644 index 0000000000..6a619903d1 --- /dev/null +++ b/.changeset/four-masks-jam.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': minor +--- + +`perform` function that does parsing, validation, context assembly and execution/subscription diff --git a/.changeset/lovely-clocks-type.md b/.changeset/lovely-clocks-type.md new file mode 100644 index 0000000000..52166356be --- /dev/null +++ b/.changeset/lovely-clocks-type.md @@ -0,0 +1,12 @@ +--- +'@envelop/core': major +--- + +We have built the new `envelop` to be engine agnostic. `graphql-js` is no longer a peer dependency. Now you can use any spec compliant GraphQL engine with `envelop` and get the benefit of building a plugin system. + +```diff ++ import { parse, validate, execute, subscribe } from 'graphql'; + +- const getEnveloped = envelop([ ... ]) ++ const getEnveloped = envelop({ parse, validate, execute, subscribe, plugins: [ ... ] }) +``` diff --git a/.changeset/neat-spoons-play.md b/.changeset/neat-spoons-play.md new file mode 100644 index 0000000000..bb69f87659 --- /dev/null +++ b/.changeset/neat-spoons-play.md @@ -0,0 +1,5 @@ +--- +'@envelop/on-resolve': major +--- + +Plugin allowing you to hook into resolves of every field in the GraphQL schema. diff --git a/.changeset/nervous-seas-own.md b/.changeset/nervous-seas-own.md new file mode 100644 index 0000000000..88250d82b7 --- /dev/null +++ b/.changeset/nervous-seas-own.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': major +--- + +Rename `useLazyLoadedSchema` to `useSchemaByContext` since the original name was vert misleading. diff --git a/.changeset/quiet-mice-jam.md b/.changeset/quiet-mice-jam.md new file mode 100644 index 0000000000..fe54fc024d --- /dev/null +++ b/.changeset/quiet-mice-jam.md @@ -0,0 +1,27 @@ +--- +'@envelop/core': major +--- + +Remove `enableIf` utility in favor of more type safe way to conditionally enable plugins. It wasn't a great experience to have a utility + +We can easily replace usage like this: + +```diff +- import { envelop, useMaskedErrors, enableIf } from '@envelop/core' ++ import { envelop, useMaskedErrors } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' + +const isProd = process.env.NODE_ENV === 'production' + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + // This plugin is enabled only in production +- enableIf(isProd, useMaskedErrors()) ++ isProd && useMaskedErrors() + ] +}) +``` diff --git a/.changeset/rude-cats-peel.md b/.changeset/rude-cats-peel.md new file mode 100644 index 0000000000..77ab8534ac --- /dev/null +++ b/.changeset/rude-cats-peel.md @@ -0,0 +1,10 @@ +--- +'@envelop/core': major +--- + +Remove `handleValidationErrors` and `handleParseErrors` options from `useMaskedErrors`. + +> ONLY masking validation errors OR ONLY disabling introspection errors does not make sense, as both can be abused for reverse-engineering the GraphQL schema (see https://github.com/nikitastupin/clairvoyance for reverse-engineering the schema based on validation error suggestions). +> https://github.com/n1ru4l/envelop/issues/1482#issue-1340015060 + +Rename `formatError` function option to `maskError` diff --git a/.changeset/silent-impalas-retire.md b/.changeset/silent-impalas-retire.md new file mode 100644 index 0000000000..303ae2e14d --- /dev/null +++ b/.changeset/silent-impalas-retire.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': minor +--- + +respond to context, parse and validate errors in `useErrorHandler` plugin diff --git a/.eslintrc.json b/.eslintrc.json index 4c797a04e6..c09453752d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,13 @@ { "parser": "@typescript-eslint/parser", - "extends": ["eslint:recommended", "standard", "prettier", "plugin:@typescript-eslint/recommended"], - "plugins": ["@typescript-eslint", "unicorn"], + "extends": [ + "eslint:recommended", + "standard", + "prettier", + "plugin:@typescript-eslint/recommended", + "plugin:package-json/recommended" + ], + "plugins": ["@typescript-eslint", "unicorn", "package-json"], "rules": { "unicorn/filename-case": "error", "no-lonely-if": "error", @@ -9,9 +15,20 @@ "no-empty": "off", "no-console": "error", "no-prototype-builtins": "off", - "prefer-arrow-callback": ["error", { "allowNamedFunctions": true }], + "prefer-arrow-callback": [ + "error", + { + "allowNamedFunctions": true + } + ], "no-useless-constructor": "off", - "@typescript-eslint/no-unused-vars": ["warn", { "args": "none" }], + "no-use-before-define": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "none" + } + ], "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-empty-interface": "off", @@ -26,7 +43,9 @@ "unicorn/no-useless-fallback-in-spread": "error", "import/no-extraneous-dependencies": [ "error", - { "devDependencies": ["**/*.test.ts", "**/*.spec.ts", "**/test/**/*.ts"] } + { + "devDependencies": ["**/*.test.ts", "**/*.spec.ts", "**/test/**/*.ts"] + } ] }, "env": { @@ -43,6 +62,38 @@ "@typescript-eslint/no-unused-vars": "off", "import/no-extraneous-dependencies": "off" } + }, + // Disallow `graphql-js` specific things to get re-introduced in agnostic packages. + { + "files": [ + "packages/core/**", + "packages/types/**", + "packages/plugins/apollo-datasources/**", + "packages/plugins/auth0/**", + "packages/plugins/dataloader/**", + "packages/plugins/preload-assets/**", + "packages/plugins/statsd/**" + ], + "env": { + "jest": true + }, + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "graphql", + "message": "You chose violence. Try to make it work without using GraphQL.js" + }, + { + "name": "@graphql-tools/*", + "message": "You chose violence. Try to make it work without using `graphql-tools`" + } + ] + } + ] + } } ], "ignorePatterns": ["dist", "node_modules", "dev-test", "website"] diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 86380a46f3..4cca1b1990 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,10 +5,10 @@ on: - main jobs: - dependencies: - uses: the-guild-org/shared-config/.github/workflows/changesets-dependencies.yaml@main - secrets: - githubToken: ${{ secrets.GITHUB_TOKEN }} + # dependencies: + # uses: the-guild-org/shared-config/.github/workflows/changesets-dependencies.yaml@main + # secrets: + # githubToken: ${{ secrets.GITHUB_TOKEN }} release: uses: the-guild-org/shared-config/.github/workflows/release-snapshot.yml@main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27efc754c3..d50fe456fe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,13 +48,38 @@ jobs: run: yarn ts:check unit: - name: unit / ${{matrix.os}} / node v${{matrix.node-version}} / graphql v${{matrix.graphql_version}} + name: Unit Test runs-on: ${{matrix.os}} strategy: matrix: os: [ubuntu-latest] # remove windows to speed up the tests - node-version: [12, 17] - graphql_version: [15, 16] + steps: + - name: Checkout Master + uses: actions/checkout@v2 + - name: Setup env + uses: the-guild-org/shared-config/setup@main + with: + nodeVersion: 18 + - name: Install Dependencies + run: yarn install --ignore-engines && git checkout yarn.lock + - name: Cache Jest + uses: actions/cache@v2 + with: + path: .cache/jest + key: ${{ runner.os }}-${{matrix.node-version}}-${{matrix.graphql_version}}-jest-${{ hashFiles('yarn.lock') }}-${{ hashFiles('patches/*.patch') }} + - name: Test + run: yarn test + env: + CI: true + + core: + name: Core Test / ${{matrix.os}} / node v${{matrix.node-version}} / graphql v${{matrix.graphql_version}} + runs-on: ${{matrix.os}} + strategy: + matrix: + os: [ubuntu-latest] # remove windows to speed up the tests + node-version: [14, 16, 17, 18] + graphql_version: [15, 16, 'npm:@graphql-tools/graphql@0.1.0-alpha-20220815193214-83898018'] steps: - name: Checkout Master uses: actions/checkout@v2 @@ -71,8 +96,8 @@ jobs: with: path: .cache/jest key: ${{ runner.os }}-${{matrix.node-version}}-${{matrix.graphql_version}}-jest-${{ hashFiles('yarn.lock') }}-${{ hashFiles('patches/*.patch') }} - - name: Test - run: yarn test --ci + - name: Test Core + run: yarn test:core --ci env: CI: true diff --git a/README.md b/README.md index 040b035334..3f15726aff 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ const getEnveloped = envelop({ }) ``` -The result of `envelop` is a function that allows you to get everything you need for the GraphQL execution: `parse`, `validate`, `contextBuilder` and `execute`. Use that to run the client's GraphQL queries. Here's a pseudo-code example of how it should look like: +The result of `envelop` is a function that allows you to get everything you need for the GraphQL execution. It's recommended to use the `perform` function which does parsing, validation, context assembly and execution/subscription, and returns a ready result. Here's a pseudo-code example of how it should look like: ```ts const httpServer = createServer() @@ -54,25 +54,13 @@ const httpServer = createServer() httpServer.on('request', async (req, res) => { // Here you get the alternative methods that are bundled with your plugins // You can also pass the "req" to make it available for your plugins or GraphQL context. - const { parse, validate, contextFactory, execute, schema } = getEnveloped({ req }) + const { perform } = getEnveloped({ req }) - // Parse the initial request and validate it + // Parse the initial request const { query, variables } = JSON.parse(req.payload) - const document = parse(query) - const validationErrors = validate(schema, document) - if (validationErrors.length > 0) { - return res.end(JSON.stringify({ errors: validationErrors })) - } - - // Build the context and execute - const context = await contextFactory(req) - const result = await execute({ - document, - schema, - variableValues: variables, - contextValue: context - }) + // Perform the GraphQL operation + const result = await perform({ query, variables }) // Send the response res.end(JSON.stringify(result)) diff --git a/examples/apollo-server/index.ts b/examples/apollo-server/index.ts index 5ef3d636e8..0deb297fd6 100644 --- a/examples/apollo-server/index.ts +++ b/examples/apollo-server/index.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { ApolloServer } from 'apollo-server'; -import { envelop, useSchema, useTiming } from '@envelop/core'; +import { envelop, useSchema } from '@envelop/core'; +import { parse, validate, subscribe, execute } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'; @@ -18,7 +19,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useTiming()], + parse, + validate, + subscribe, + execute, + plugins: [useSchema(schema)], }); const server = new ApolloServer({ diff --git a/examples/apollo-server/package.json b/examples/apollo-server/package.json index 48f6af6ca7..90e82f1572 100644 --- a/examples/apollo-server/package.json +++ b/examples/apollo-server/package.json @@ -9,7 +9,8 @@ "@envelop/core": "*", "apollo-server": "3.5.0", "apollo-server-core": "3.5.0", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/azure-functions/index.ts b/examples/azure-functions/index.ts index 87ff811d9c..9b7b0ef44a 100644 --- a/examples/azure-functions/index.ts +++ b/examples/azure-functions/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, subscribe, execute } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { AzureFunction, Context, HttpRequest } from '@azure/functions'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); export const index: AzureFunction = async (context: Context, req: HttpRequest): Promise => { diff --git a/examples/azure-functions/package.json b/examples/azure-functions/package.json index db521ad1e5..89532bb99d 100644 --- a/examples/azure-functions/package.json +++ b/examples/azure-functions/package.json @@ -8,7 +8,8 @@ "dependencies": { "graphql-helix": "1.8.3", "@envelop/core": "*", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@azure/functions": "1.2.3", diff --git a/examples/cloudflare-workers/index.ts b/examples/cloudflare-workers/index.ts index 41fac195fe..bf2becd792 100644 --- a/examples/cloudflare-workers/index.ts +++ b/examples/cloudflare-workers/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; import { Router } from 'worktop'; @@ -20,7 +21,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); router.add('POST', '/graphql', async (req, res) => { diff --git a/examples/cloudflare-workers/package.json b/examples/cloudflare-workers/package.json index 702ff47665..607d107be4 100644 --- a/examples/cloudflare-workers/package.json +++ b/examples/cloudflare-workers/package.json @@ -9,7 +9,8 @@ "graphql-helix": "1.8.3", "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", - "worktop": "0.7.0" + "worktop": "0.7.0", + "graphql": "16.6.0" }, "devDependencies": { "@cloudflare/workers-types": "2.2.2", diff --git a/examples/express-graphql/index.ts b/examples/express-graphql/index.ts index e06d88c2df..31994634e0 100644 --- a/examples/express-graphql/index.ts +++ b/examples/express-graphql/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import express from 'express'; import { graphqlHTTP } from 'express-graphql'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = express(); diff --git a/examples/express-graphql/package.json b/examples/express-graphql/package.json index 7c14c6c5f8..a0787a1a60 100644 --- a/examples/express-graphql/package.json +++ b/examples/express-graphql/package.json @@ -9,7 +9,8 @@ "express": "3.14.0", "express-graphql": "0.12.0", "@envelop/core": "*", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/google-cloud-functions/index.ts b/examples/google-cloud-functions/index.ts index 33fc5c7d21..fad779a9fa 100644 --- a/examples/google-cloud-functions/index.ts +++ b/examples/google-cloud-functions/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import * as functions from 'firebase-functions'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); // https://firebase.google.com/docs/functions/typescript diff --git a/examples/google-cloud-functions/package.json b/examples/google-cloud-functions/package.json index 9cc92dd45f..2affb5b9b8 100644 --- a/examples/google-cloud-functions/package.json +++ b/examples/google-cloud-functions/package.json @@ -9,7 +9,8 @@ "@envelop/core": "*", "firebase-admin": "9.9.0", "firebase-functions": "3.14.1", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "main": "lib/index.js", "devDependencies": { diff --git a/examples/graphql-helix-auth0/index.ts b/examples/graphql-helix-auth0/index.ts index 43a0b818ef..f0940177fe 100644 --- a/examples/graphql-helix-auth0/index.ts +++ b/examples/graphql-helix-auth0/index.ts @@ -2,6 +2,7 @@ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; import { envelop, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { useAuth0 } from '@envelop/auth0'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -40,6 +41,10 @@ const auth0Config = { }; const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useAuth0({ diff --git a/examples/graphql-helix-auth0/package.json b/examples/graphql-helix-auth0/package.json index fce86e90d9..ce79381df0 100644 --- a/examples/graphql-helix-auth0/package.json +++ b/examples/graphql-helix-auth0/package.json @@ -10,7 +10,8 @@ "@envelop/core": "*", "@envelop/auth0": "*", "graphql-helix": "1.8.3", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-helix-defer-stream/index.ts b/examples/graphql-helix-defer-stream/index.ts index ccb20bf76b..c071bf4557 100644 --- a/examples/graphql-helix-defer-stream/index.ts +++ b/examples/graphql-helix-defer-stream/index.ts @@ -1,7 +1,8 @@ /* eslint-disable no-console */ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; +import { envelop, useLogger, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; const sleep = (t = 1000) => new Promise(resolve => setTimeout(resolve, t)); @@ -140,7 +141,11 @@ const graphiQLContent = /* GraphQL */ ` `; const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/examples/graphql-helix/index.ts b/examples/graphql-helix/index.ts index 20d936390a..d64beb126a 100644 --- a/examples/graphql-helix/index.ts +++ b/examples/graphql-helix/index.ts @@ -1,7 +1,8 @@ /* eslint-disable no-console */ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ @@ -18,7 +19,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/examples/graphql-helix/package.json b/examples/graphql-helix/package.json index e73837b229..b3c4d0bf35 100644 --- a/examples/graphql-helix/package.json +++ b/examples/graphql-helix/package.json @@ -9,7 +9,8 @@ "fastify": "3.14.0", "@envelop/core": "*", "graphql-helix": "1.8.3", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-socket.io/index.ts b/examples/graphql-socket.io/index.ts index a167653500..57b4e6969f 100644 --- a/examples/graphql-socket.io/index.ts +++ b/examples/graphql-socket.io/index.ts @@ -1,7 +1,8 @@ import { Server } from 'socket.io'; import * as http from 'http'; import { registerSocketIOGraphQLServer } from '@n1ru4l/socket-io-graphql-server'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ @@ -17,9 +18,7 @@ const schema = makeExecutableSchema({ }, }); -const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], -}); +const getEnveloped = envelop({ parse, validate, execute, subscribe, plugins: [useSchema(schema), useLogger()] }); const httpServer = http.createServer(); const socketServer = new Server(httpServer); diff --git a/examples/graphql-socket.io/package.json b/examples/graphql-socket.io/package.json index bcb8db5b51..2e2c4a0f48 100644 --- a/examples/graphql-socket.io/package.json +++ b/examples/graphql-socket.io/package.json @@ -13,7 +13,8 @@ "graphql-ws": "^4.4.2", "ws": "7.4.5", "socket.io": "4.1.2", - "socket.io-client": "4.1.2" + "socket.io-client": "4.1.2", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-sse/index.ts b/examples/graphql-sse/index.ts index ae9f95fde4..0a0090724e 100644 --- a/examples/graphql-sse/index.ts +++ b/examples/graphql-sse/index.ts @@ -1,5 +1,6 @@ import http from 'http'; -import { envelop, useSchema, useLogger, useTiming } from '@envelop/core'; +import { envelop, useSchema, useLogger } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { createHandler } from 'graphql-sse'; @@ -30,7 +31,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const handler = createHandler({ diff --git a/examples/graphql-sse/package.json b/examples/graphql-sse/package.json index da4933b4c3..4e86e72622 100644 --- a/examples/graphql-sse/package.json +++ b/examples/graphql-sse/package.json @@ -8,7 +8,8 @@ "dependencies": { "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", - "graphql-sse": "^1.0.1" + "graphql-sse": "^1.0.1", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-ws/index.ts b/examples/graphql-ws/index.ts index 87020b79c3..2cf47ad193 100644 --- a/examples/graphql-ws/index.ts +++ b/examples/graphql-ws/index.ts @@ -1,5 +1,6 @@ -import { envelop, useSchema, useLogger, useTiming } from '@envelop/core'; +import { envelop, useSchema, useLogger } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { parse, validate, execute, subscribe } from 'graphql'; import ws from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; @@ -30,7 +31,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); useServer( diff --git a/examples/graphql-ws/package.json b/examples/graphql-ws/package.json index 65e0ac8c2e..66f5c1d33e 100644 --- a/examples/graphql-ws/package.json +++ b/examples/graphql-ws/package.json @@ -9,7 +9,8 @@ "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", "graphql-ws": "^4.4.2", - "ws": "7.4.5" + "ws": "7.4.5", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/lambda-aws/index.ts b/examples/lambda-aws/index.ts index 4d86131d65..ae790dc82e 100644 --- a/examples/lambda-aws/index.ts +++ b/examples/lambda-aws/index.ts @@ -1,5 +1,6 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { parse, validate, execute, subscribe } from 'graphql'; import { APIGatewayProxyHandler } from 'aws-lambda'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); export const lambdaHandler: APIGatewayProxyHandler = async event => { diff --git a/examples/lambda-aws/package.json b/examples/lambda-aws/package.json index 9a5781a5d6..fbbd4a80b3 100644 --- a/examples/lambda-aws/package.json +++ b/examples/lambda-aws/package.json @@ -9,7 +9,8 @@ "graphql-helix": "1.8.3", "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", - "aws-sdk": "2.918.0" + "aws-sdk": "2.918.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/aws-lambda": "8.10.76", diff --git a/examples/nexus/index.ts b/examples/nexus/index.ts index 36ffe618ff..5b68b07c2e 100644 --- a/examples/nexus/index.ts +++ b/examples/nexus/index.ts @@ -1,7 +1,8 @@ /* eslint-disable no-console */ import 'reflect-metadata'; import fastify from 'fastify'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; import { arg, enumType, intArg, interfaceType, makeSchema, objectType, queryType, stringArg, list } from 'nexus'; @@ -51,7 +52,11 @@ const schema = makeSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/examples/simple-http/index.ts b/examples/simple-http/index.ts index cdeed847de..d27350e3ad 100644 --- a/examples/simple-http/index.ts +++ b/examples/simple-http/index.ts @@ -1,5 +1,6 @@ import { createServer } from 'http'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ @@ -16,11 +17,15 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const server = createServer((req, res) => { - const { parse, validate, contextFactory, execute, schema } = getEnveloped({ req }); + const { perform } = getEnveloped({ req }); let payload = ''; req.on('data', chunk => { @@ -29,26 +34,8 @@ const server = createServer((req, res) => { req.on('end', async () => { const { query, variables } = JSON.parse(payload); - const document = parse(query); - const validationErrors = validate(schema, document); - if (validationErrors.length > 0) { - res.end( - JSON.stringify({ - errors: validationErrors, - }) - ); - - return; - } - - const context = await contextFactory(); - const result = await execute({ - document, - schema, - variableValues: variables, - contextValue: context, - }); + const result = await perform({ query, variables }); res.end(JSON.stringify(result)); }); diff --git a/examples/simple-http/package.json b/examples/simple-http/package.json index 61e4919a9a..c7e6746631 100644 --- a/examples/simple-http/package.json +++ b/examples/simple-http/package.json @@ -7,7 +7,8 @@ "license": "MIT", "dependencies": { "@envelop/core": "*", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/typegraphql/index.ts b/examples/typegraphql/index.ts index 1561ee72ed..3ca665bdb1 100644 --- a/examples/typegraphql/index.ts +++ b/examples/typegraphql/index.ts @@ -1,8 +1,9 @@ /* eslint-disable no-console */ import 'reflect-metadata'; import fastify from 'fastify'; -import { envelop, useLogger, useAsyncSchema, useTiming } from '@envelop/core'; -import { Field, ObjectType, buildSchema, ID, Resolver, Query, Arg } from 'type-graphql'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; +import { Field, ObjectType, buildSchemaSync, ID, Resolver, Query, Arg } from 'type-graphql'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; @ObjectType() @@ -40,16 +41,18 @@ class RecipeResolver { } } -// You can also use `buildSchemaSync` and `useSchema` plugin const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ - useAsyncSchema( - buildSchema({ + useSchema( + buildSchemaSync({ resolvers: [RecipeResolver], }) ), useLogger(), - useTiming(), ], }); diff --git a/examples/with-esm/package.json b/examples/with-esm/package.json index 179779f7af..d6980c3849 100644 --- a/examples/with-esm/package.json +++ b/examples/with-esm/package.json @@ -10,7 +10,8 @@ "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", "fastify": "3.14.0", - "graphql-helix": "1.8.3" + "graphql-helix": "1.8.3", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.12.2", diff --git a/examples/with-esm/src/index.ts b/examples/with-esm/src/index.ts index 699cee7e1a..b4ab0ab96a 100644 --- a/examples/with-esm/src/index.ts +++ b/examples/with-esm/src/index.ts @@ -1,8 +1,9 @@ /* eslint-disable no-console */ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { parse, validate, execute, subscribe } from 'graphql'; const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` @@ -18,7 +19,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/package.json b/package.json index 89f3459392..f5ba53f559 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "build": "bob build", "ts:check": "tsc --noEmit", "test": "jest", + "test:core": "jest ./packages/core --coverage", "test:ci": "jest --coverage", "prerelease": "yarn build", "release": "changeset publish", @@ -45,16 +46,16 @@ "@babel/plugin-proposal-decorators": "7.17.2", "@babel/preset-env": "7.16.11", "@babel/preset-typescript": "7.16.7", - "@changesets/cli": "2.24.2", "@changesets/changelog-github": "0.4.6", + "@changesets/cli": "2.24.2", "@graphql-tools/schema": "8.5.0", "@theguild/prettier-config": "0.0.2", "@types/benchmark": "2.1.1", "@types/jest": "27.4.1", "@types/k6": "0.36.0", "@types/node": "16.11.26", - "@typescript-eslint/eslint-plugin": "5.27.0", - "@typescript-eslint/parser": "5.27.0", + "@typescript-eslint/eslint-plugin": "5.36.2", + "@typescript-eslint/parser": "5.36.2", "apollo-server": "3.5.0", "benchmark": "2.1.4", "bob-the-bundler": "4.0.0", @@ -64,6 +65,7 @@ "eslint-config-standard": "17.0.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-n": "15.2.1", + "eslint-plugin-package-json": "^0.1.4", "eslint-plugin-promise": "6.0.0", "eslint-plugin-unicorn": "43.0.0", "faker": "5.5.3", diff --git a/packages/core/README.md b/packages/core/README.md index cc86e2f198..e35719ce8d 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -5,11 +5,9 @@ This is the core package for Envelop. You can find a complete documentation here ### Built-in plugins - [`useSchema`](./docs/use-schema.md) -- [`useAsyncSchema`](./docs/use-async-schema.md) - [`useLazyLoadedSchema`](./docs/use-lazy-loaded-schema.md) - [`useErrorHandler`](./docs/use-error-handler.md) - [`useExtendContext`](./docs/use-extend-context.md) - [`useLogger`](./docs/use-logger.md) - [`useMaskedErrors`](./docs/use-masked-errors.md) - [`usePayloadFormatter`](./docs/use-payload-formatter.md) -- [`useTiming`](./docs/use-timing.md) diff --git a/packages/core/docs/use-async-schema.md b/packages/core/docs/use-async-schema.md deleted file mode 100644 index e4754c65d3..0000000000 --- a/packages/core/docs/use-async-schema.md +++ /dev/null @@ -1,19 +0,0 @@ -#### `useAsyncSchema` - -This plugin is the simplest plugin for specifying your GraphQL schema. You can specify a schema created from any tool that emits `Promise` object. - -```ts -import { envelop, useAsyncSchema } from '@envelop/core' -import { buildSchema } from 'graphql' - -const getSchema = async (): Promise => { - // return schema when it's ready -} - -const getEnveloped = envelop({ - plugins: [ - useAsyncSchema(getSchema()) - // ... other plugins ... - ] -}) -``` diff --git a/packages/core/docs/use-error-handler.md b/packages/core/docs/use-error-handler.md index a6b16d4b9a..739b59c6cb 100644 --- a/packages/core/docs/use-error-handler.md +++ b/packages/core/docs/use-error-handler.md @@ -4,9 +4,13 @@ This plugin triggers a custom function when execution encounters an error. ```ts import { envelop, useErrorHandler } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useErrorHandler((errors, args) => { // This callback is called once, containing all GraphQLError emitted during execution phase diff --git a/packages/core/docs/use-extend-context.md b/packages/core/docs/use-extend-context.md index 7b160f7ef9..5267cbddc7 100644 --- a/packages/core/docs/use-extend-context.md +++ b/packages/core/docs/use-extend-context.md @@ -4,9 +4,13 @@ Easily extends the context with custom fields. ```ts import { envelop, useExtendContext } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useExtendContext(async contextSoFar => { return { diff --git a/packages/core/docs/use-logger.md b/packages/core/docs/use-logger.md index d2893b25e8..39a44e15d4 100644 --- a/packages/core/docs/use-logger.md +++ b/packages/core/docs/use-logger.md @@ -4,9 +4,13 @@ Logs parameters and information about the execution phases. You can easily plug ```ts import { envelop, useLogger } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useLogger({ logFn: (eventName, args) => { diff --git a/packages/core/docs/use-masked-errors.md b/packages/core/docs/use-masked-errors.md index 628b6b16af..0bb09e383a 100644 --- a/packages/core/docs/use-masked-errors.md +++ b/packages/core/docs/use-masked-errors.md @@ -3,8 +3,8 @@ Prevent unexpected error messages from leaking to the GraphQL clients. ```ts -import { envelop, useSchema, useMaskedErrors, EnvelopError } from '@envelop/core' -import { makeExecutableSchema } from 'graphql' +import { envelop, useSchema, useMaskedErrors } from '@envelop/core' +import { makeExecutableSchema, GraphQLError } from 'graphql' const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` @@ -17,13 +17,13 @@ const schema = makeExecutableSchema({ resolvers: { Query: { something: () => { - throw new EnvelopError('Error that is propagated to the clients.') + throw new GraphQLError('Error that is propagated to the clients.') }, somethingElse: () => { throw new Error("Unsafe error that will be masked as 'Unexpected Error.'.") }, somethingSpecial: () => { - throw new EnvelopError('The error will have an extensions field.', { + throw new GraphQLError('The error will have an extensions field.', { code: 'ERR_CODE', randomNumber: 123 }) @@ -48,8 +48,10 @@ const getEnveloped = envelop({ Or provide a custom formatter when masking the output: ```ts -export const customFormatError: FormatErrorHandler = err => { - if (err.originalError && err.originalError instanceof EnvelopError === false) { +import { isGraphQLError, MaskError } from '@envelop/core' + +export const customFormatError: MaskError = err => { + if (isGraphQLError(err)) { return new GraphQLError('Sorry, something went wrong.') } @@ -57,6 +59,6 @@ export const customFormatError: FormatErrorHandler = err => { } const getEnveloped = envelop({ - plugins: [useSchema(schema), useMaskedErrors({ formatError: customFormatError })] + plugins: [useSchema(schema), useMaskedErrors({ maskErrorFn: customFormatError })] }) ``` diff --git a/packages/core/docs/use-payload-formatter.md b/packages/core/docs/use-payload-formatter.md index 46373890cd..b83748ec59 100644 --- a/packages/core/docs/use-payload-formatter.md +++ b/packages/core/docs/use-payload-formatter.md @@ -6,9 +6,13 @@ The second argument `executionArgs` provides additional information for your for ```ts import { envelop, usePayloadFormatter } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ usePayloadFormatter((result, executionArgs) => { // Return a modified result here, diff --git a/packages/core/docs/use-lazy-loaded-schema.md b/packages/core/docs/use-schema-by-context.md similarity index 70% rename from packages/core/docs/use-lazy-loaded-schema.md rename to packages/core/docs/use-schema-by-context.md index 783116a677..cff850017d 100644 --- a/packages/core/docs/use-lazy-loaded-schema.md +++ b/packages/core/docs/use-schema-by-context.md @@ -3,8 +3,8 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can specify a schema created from any tool that emits `GraphQLSchema` object, and you can choose to load the schema based on the initial context (or the incoming request). ```ts -import { envelop, useLazyLoadedSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { envelop, useSchemaByContext } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' async function getSchema({ req }): GraphQLSchema { if (req.isAdmin) { @@ -15,8 +15,12 @@ async function getSchema({ req }): GraphQLSchema { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ - useLazyLoadedSchema(getSchema) + useSchemaByContext(getSchema) // ... other plugins ... ] }) diff --git a/packages/core/docs/use-schema.md b/packages/core/docs/use-schema.md index 10d1b8719c..4671d37817 100644 --- a/packages/core/docs/use-schema.md +++ b/packages/core/docs/use-schema.md @@ -4,11 +4,15 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can s ```ts import { envelop, useSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const mySchema = buildSchema(/* ... */) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(mySchema) // ... other plugins ... diff --git a/packages/core/docs/use-timing.md b/packages/core/docs/use-timing.md deleted file mode 100644 index 10443e899d..0000000000 --- a/packages/core/docs/use-timing.md +++ /dev/null @@ -1,24 +0,0 @@ -#### `useTiming` - -Simple time metric collection, for every phase in your execution. You can easily customize the behavior of each timing measurement. By default, the timing is printed to the log, using `console.log`. - -```ts -import { envelop, useTiming } from '@envelop/core' -import { buildSchema } from 'graphql' - -const getEnveloped = envelop({ - plugins: [ - useTiming({ - // All options are optional. By default it just print it to the log. - // ResultTiming is an object built with { ms, ns } (milliseconds and nanoseconds) - onContextBuildingMeasurement: (timing: ResultTiming) => {}, - onExecutionMeasurement: (args: ExecutionArgs, timing: ResultTiming) => {}, - onSubscriptionMeasurement: (args: SubscriptionArgs, timing: ResultTiming) => {}, - onParsingMeasurement: (source: Source | string, timing: ResultTiming) => {}, - onValidationMeasurement: (document: DocumentNode, timing: ResultTiming) => {}, - onResolverMeasurement: (info: GraphQLResolveInfo, timing: ResultTiming) => {} - }) - // ... other plugins ... - ] -}) -``` diff --git a/packages/core/package.json b/packages/core/package.json index 1e82fb121c..879044bdc4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,9 +65,6 @@ "graphql": "16.3.0", "typescript": "4.7.4" }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" - }, "buildOptions": { "input": "./src/index.ts" }, diff --git a/packages/core/src/create.ts b/packages/core/src/create.ts index d47665016d..6e5d5e0955 100644 --- a/packages/core/src/create.ts +++ b/packages/core/src/create.ts @@ -1,23 +1,46 @@ -import { GetEnvelopedFn, ComposeContext, Plugin, ArbitraryObject } from '@envelop/types'; -import { isPluginEnabled, PluginOrDisabledPlugin } from './enable-if.js'; +import { + GetEnvelopedFn, + ComposeContext, + Plugin, + ArbitraryObject, + ExecuteFunction, + SubscribeFunction, + ParseFunction, + ValidateFunction, + Optional, +} from '@envelop/types'; import { createEnvelopOrchestrator, EnvelopOrchestrator } from './orchestrator.js'; -import { traceOrchestrator } from './traced-orchestrator.js'; -export function envelop[]>(options: { - plugins: Array; - enableInternalTracing?: boolean; -}): GetEnvelopedFn> { - const plugins = options.plugins.filter(isPluginEnabled); - let orchestrator = createEnvelopOrchestrator>(plugins); +type ExcludeFalsy = Exclude[]; + +function notEmpty(value: Optional): value is T { + return value != null; +} - if (options.enableInternalTracing) { - orchestrator = traceOrchestrator(orchestrator); - } +export function envelop>[]>(options: { + plugins: PluginsType; + enableInternalTracing?: boolean; + parse: ParseFunction; + execute: ExecuteFunction; + validate: ValidateFunction; + subscribe: SubscribeFunction; +}): GetEnvelopedFn>> { + const plugins = options.plugins.filter(notEmpty); + const orchestrator = createEnvelopOrchestrator>>({ + plugins, + parse: options.parse, + execute: options.execute, + validate: options.validate, + subscribe: options.subscribe, + }); const getEnveloped = ( initialContext: TInitialContext = {} as TInitialContext ) => { - const typedOrchestrator = orchestrator as EnvelopOrchestrator>; + const typedOrchestrator = orchestrator as EnvelopOrchestrator< + TInitialContext, + ComposeContext> + >; typedOrchestrator.init(initialContext); return { @@ -27,10 +50,11 @@ export function envelop[]>(options: { execute: typedOrchestrator.execute, subscribe: typedOrchestrator.subscribe, schema: typedOrchestrator.getCurrentSchema(), + perform: typedOrchestrator.perform(initialContext), }; }; getEnveloped._plugins = plugins; - return getEnveloped as GetEnvelopedFn>; + return getEnveloped as GetEnvelopedFn>>; } diff --git a/packages/core/src/enable-if.ts b/packages/core/src/enable-if.ts deleted file mode 100644 index 5c1607c9c2..0000000000 --- a/packages/core/src/enable-if.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Plugin } from '@envelop/types'; - -/** - * This enum is used only internally in order to create nominal type for the disabled plugin - */ -enum EnableIfBranded { - DisabledPlugin, -} - -export type PluginOrDisabledPlugin = Plugin | EnableIfBranded.DisabledPlugin; - -export function isPluginEnabled(t: PluginOrDisabledPlugin): t is Plugin { - return t !== EnableIfBranded.DisabledPlugin && t !== null; -} - -/** - * Utility function to enable a plugin. - */ -export function enableIf = {}>( - condition: boolean, - plugin: Plugin | (() => Plugin) -): PluginOrDisabledPlugin { - if (condition) { - return typeof plugin === 'function' ? plugin() : plugin; - } - return EnableIfBranded.DisabledPlugin; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ffbb821aa6..0789738c90 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,11 +3,7 @@ export * from './create.js'; export * from './utils.js'; export * from './plugins/use-envelop.js'; export * from './plugins/use-logger.js'; -export * from './plugins/use-timing.js'; export * from './plugins/use-schema.js'; export * from './plugins/use-error-handler.js'; export * from './plugins/use-extend-context.js'; export * from './plugins/use-payload-formatter.js'; -export * from './plugins/use-masked-errors.js'; -export * from './plugins/use-immediate-introspection.js'; -export * from './enable-if.js'; diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 759e22d91e..a12154491b 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -10,7 +10,6 @@ import { OnExecuteDoneHook, OnExecuteHook, OnParseHook, - OnResolverCalledHook, OnSubscribeHook, OnValidateHook, Plugin, @@ -28,20 +27,13 @@ import { SubscribeErrorHook, DefaultContext, Maybe, -} from '@envelop/types'; -import { - DocumentNode, - execute, + ParseFunction, + ValidateFunction, ExecutionResult, - GraphQLError, - GraphQLSchema, - parse, - specifiedRules, - subscribe, - validate, - ValidationRule, -} from 'graphql'; -import { prepareTracedSchema, resolversHooksSymbol } from './traced-schema.js'; + PerformFunction, + OnPerformHook, + OnPerformDoneHook, +} from '@envelop/types'; import { errorAsyncIterator, finalAsyncIterator, @@ -49,6 +41,7 @@ import { makeSubscribe, mapAsyncIterator, isAsyncIterable, + isSubscriptionOperation, } from './utils.js'; export type EnvelopOrchestrator< @@ -61,28 +54,32 @@ export type EnvelopOrchestrator< execute: ReturnType>['execute']; subscribe: ReturnType>['subscribe']; contextFactory: EnvelopContextFnWrapper>['contextFactory'], PluginsContext>; - getCurrentSchema: () => Maybe; + getCurrentSchema: () => Maybe; + perform: EnvelopContextFnWrapper; }; -export function createEnvelopOrchestrator( - plugins: Plugin[] -): EnvelopOrchestrator { - let schema: GraphQLSchema | undefined | null = null; +type EnvelopOrchestratorOptions = { + plugins: Plugin[]; + parse: ParseFunction; + execute: ExecuteFunction; + subscribe: SubscribeFunction; + validate: ValidateFunction; +}; + +export function createEnvelopOrchestrator({ + plugins, + parse, + execute, + subscribe, + validate, +}: EnvelopOrchestratorOptions): EnvelopOrchestrator { + let schema: any | undefined | null = null; let initDone = false; - const onResolversHandlers: OnResolverCalledHook[] = []; - for (const plugin of plugins) { - if (plugin.onResolverCalled) { - onResolversHandlers.push(plugin.onResolverCalled); - } - } // Define the initial method for replacing the GraphQL schema, this is needed in order // to allow setting the schema from the onPluginInit callback. We also need to make sure // here not to call the same plugin that initiated the schema switch. - const replaceSchema = (newSchema: GraphQLSchema, ignorePluginIndex = -1) => { - if (onResolversHandlers.length) { - prepareTracedSchema(newSchema); - } + const replaceSchema = (newSchema: any, ignorePluginIndex = -1) => { schema = newSchema; if (initDone) { @@ -123,15 +120,17 @@ export function createEnvelopOrchestrator subscribe: [] as OnSubscribeHook[], execute: [] as OnExecuteHook[], context: [] as OnContextBuildingHook[], + perform: [] as OnPerformHook[], }; - for (const { onContextBuilding, onExecute, onParse, onSubscribe, onValidate, onEnveloped } of plugins) { + for (const { onContextBuilding, onExecute, onParse, onSubscribe, onValidate, onEnveloped, onPerform } of plugins) { onEnveloped && beforeCallbacks.init.push(onEnveloped); onContextBuilding && beforeCallbacks.context.push(onContextBuilding); onExecute && beforeCallbacks.execute.push(onExecute); onParse && beforeCallbacks.parse.push(onParse); onSubscribe && beforeCallbacks.subscribe.push(onSubscribe); onValidate && beforeCallbacks.validate.push(onValidate); + onPerform && beforeCallbacks.perform.push(onPerform); } const init: EnvelopOrchestrator['init'] = initialContext => { @@ -152,7 +151,7 @@ export function createEnvelopOrchestrator const customParse: EnvelopContextFnWrapper = beforeCallbacks.parse.length ? initialContext => (source, parseOptions) => { - let result: DocumentNode | Error | null = null; + let result: any | Error | null = null; let parseFn: typeof parse = parse; const context = initialContext; const afterCalls: AfterParseHook[] = []; @@ -211,9 +210,9 @@ export function createEnvelopOrchestrator const customValidate: EnvelopContextFnWrapper = beforeCallbacks.validate.length ? initialContext => (schema, documentAST, rules, typeInfo, validationOptions) => { - let actualRules: undefined | ValidationRule[] = rules ? [...rules] : undefined; + let actualRules: undefined | any[] = rules ? [...rules] : undefined; let validateFn = validate; - let result: null | readonly GraphQLError[] = null; + let result: null | readonly any[] = null; const context = initialContext; const afterCalls: AfterValidateHook[] = []; @@ -234,7 +233,10 @@ export function createEnvelopOrchestrator validateFn, addValidationRule: rule => { if (!actualRules) { - actualRules = [...specifiedRules]; + // Ideally we should provide default validation rules here. + // eslint-disable-next-line no-console + console.warn('No default validation rules provided.'); + actualRules = []; } actualRules.push(rule); @@ -254,6 +256,10 @@ export function createEnvelopOrchestrator result = validateFn(schema, documentAST, actualRules, typeInfo, validationOptions); } + if (!result) { + return; + } + const valid = result.length === 0; for (const afterCb of afterCalls) { @@ -331,7 +337,7 @@ export function createEnvelopOrchestrator } : initialContext => orchestratorCtx => orchestratorCtx ? { ...initialContext, ...orchestratorCtx } : initialContext; - const useCustomSubscribe = beforeCallbacks.subscribe.length || onResolversHandlers.length; + const useCustomSubscribe = beforeCallbacks.subscribe.length; const customSubscribe = useCustomSubscribe ? makeSubscribe(async args => { @@ -372,10 +378,6 @@ export function createEnvelopOrchestrator } } - if (onResolversHandlers.length) { - context[resolversHooksSymbol] = onResolversHandlers; - } - if (result === undefined) { result = await subscribeFn({ ...args, @@ -384,6 +386,9 @@ export function createEnvelopOrchestrator // Can be removed once we drop support for GraphQL.js 15 }); } + if (!result) { + return; + } const onNextHandler: OnSubscribeResultResultOnNextHook[] = []; const onEndHandler: OnSubscribeResultResultOnEndHook[] = []; @@ -445,7 +450,7 @@ export function createEnvelopOrchestrator }) : makeSubscribe(subscribe as any); - const useCustomExecute = beforeCallbacks.execute.length || onResolversHandlers.length; + const useCustomExecute = beforeCallbacks.execute.length; const customExecute = useCustomExecute ? makeExecute(async args => { @@ -490,10 +495,6 @@ export function createEnvelopOrchestrator } } - if (onResolversHandlers.length) { - context[resolversHooksSymbol] = onResolversHandlers; - } - if (result === undefined) { result = (await executeFn({ ...args, @@ -562,6 +563,99 @@ export function createEnvelopOrchestrator } } + const customPerform: EnvelopContextFnWrapper = initialContext => { + const parse = customParse(initialContext); + const validate = customValidate(initialContext); + const contextFactory = customContextFactory(initialContext); + + return async (params, contextExtension) => { + let context = initialContext; + + let earlyResult: AsyncIterableIteratorOrValue | null = null; + const onDones: OnPerformDoneHook[] = []; + for (const onPerform of beforeCallbacks.perform) { + const after = await onPerform({ + context, + extendContext: extension => { + Object.assign(context, extension); + }, + params, + setParams: newParams => { + params = newParams; + }, + setResult: result => { + earlyResult = result; + }, + }); + after?.onPerformDone && onDones.push(after.onPerformDone); + } + const done = (result: AsyncIterableIteratorOrValue) => { + for (const onDone of onDones) { + onDone({ + context, // either the initial or factory context, depenends when done + result, + setResult: newResult => { + result = newResult; + }, + }); + } + return result; + }; + + if (earlyResult) { + return done(earlyResult); + } + + let document; + try { + document = parse(params.query); + } catch (err) { + if (err instanceof Error && err.name === 'GraphQLError') { + // only graphql errors can be a part of the result + return done({ errors: [err] }); + } + // everything else bubble + throw err; + } + + try { + const validationErrors = validate(schema, document); + if (validationErrors.length) { + return done({ errors: validationErrors }); + } + } catch (err) { + if (err instanceof Error && err.name === 'GraphQLError') { + // only graphql errors can be a part of the result + return done({ errors: [err] }); + } + // everything else bubble + throw err; + } + + context = await contextFactory(contextExtension); + + if (isSubscriptionOperation(document, params.operationName)) { + return done( + await customSubscribe({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }) + ); + } + + return done( + await customExecute({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }) + ); + }; + }; + return { getCurrentSchema() { return schema; @@ -572,5 +666,6 @@ export function createEnvelopOrchestrator execute: customExecute as ExecuteFunction, subscribe: customSubscribe, contextFactory: customContextFactory, + perform: customPerform, }; } diff --git a/packages/core/src/plugins/use-error-handler.ts b/packages/core/src/plugins/use-error-handler.ts index 2c6394814c..bbc33b884c 100644 --- a/packages/core/src/plugins/use-error-handler.ts +++ b/packages/core/src/plugins/use-error-handler.ts @@ -1,8 +1,16 @@ -import { Plugin, DefaultContext, TypedExecutionArgs } from '@envelop/types'; -import { ExecutionResult, GraphQLError } from 'graphql'; +import { Plugin, DefaultContext, TypedExecutionArgs, ExecutionResult } from '@envelop/types'; import { handleStreamOrSingleExecutionResult } from '../utils.js'; +import { isGraphQLError, SerializableGraphQLErrorLike } from './use-masked-errors.js'; -export type ErrorHandler = (errors: readonly GraphQLError[], context: Readonly) => void; +export type ErrorHandler = ({ + errors, + context, + phase, +}: { + errors: readonly Error[] | readonly SerializableGraphQLErrorLike[]; + context: Readonly; + phase: 'parse' | 'validate' | 'context' | 'execution'; +}) => void; type ErrorHandlerCallback = { result: ExecutionResult; @@ -13,7 +21,7 @@ const makeHandleResult = >(errorHandler: ErrorHandler) => ({ result, args }: ErrorHandlerCallback) => { if (result.errors?.length) { - errorHandler(result.errors, args); + errorHandler({ errors: result.errors, context: args, phase: 'execution' }); } }; @@ -22,6 +30,30 @@ export const useErrorHandler = >( ): Plugin => { const handleResult = makeHandleResult(errorHandler); return { + onParse() { + return function onParseEnd({ result, context }) { + if (result instanceof Error) { + errorHandler({ errors: [result], context, phase: 'parse' }); + } + }; + }, + onValidate() { + return function onValidateEnd({ valid, result, context }) { + if (valid === false && result.length > 0) { + errorHandler({ errors: result as Error[], context, phase: 'validate' }); + } + }; + }, + onPluginInit(context) { + context.registerContextErrorHandler(({ error }) => { + if (isGraphQLError(error)) { + errorHandler({ errors: [error], context, phase: 'context' }); + } else { + // @ts-expect-error its not an error at this point so we just create a new one - can we handle this better? + errorHandler({ errors: [new Error(error)], context, phase: 'context' }); + } + }); + }, onExecute() { return { onExecuteDone(payload) { diff --git a/packages/core/src/plugins/use-masked-errors.ts b/packages/core/src/plugins/use-masked-errors.ts index 2328ae1bc5..ebc59ca740 100644 --- a/packages/core/src/plugins/use-masked-errors.ts +++ b/packages/core/src/plugins/use-masked-errors.ts @@ -1,120 +1,95 @@ -import { Plugin } from '@envelop/types'; -import { ExecutionResult, GraphQLError, GraphQLErrorExtensions } from 'graphql'; +import { Plugin, ExecutionResult } from '@envelop/types'; import { handleStreamOrSingleExecutionResult } from '../utils.js'; export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.'; -export class EnvelopError extends GraphQLError { - constructor(message: string, extensions?: GraphQLErrorExtensions) { - super(message, undefined, undefined, undefined, undefined, undefined, extensions); - } +export type MaskError = (error: unknown, message: string) => Error; + +export type SerializableGraphQLErrorLike = Error & { + name: 'GraphQLError'; + toJSON(): { message: string }; + extensions?: Record; +}; + +export function isGraphQLError(error: unknown): error is Error & { originalError?: Error } { + return error instanceof Error && error.name === 'GraphQLError'; } -export type FormatErrorHandler = (error: GraphQLError | unknown, message: string, isDev: boolean) => GraphQLError; +function createSerializableGraphQLError( + message: string, + originalError: unknown, + isDev: boolean +): SerializableGraphQLErrorLike { + const error = new Error(message) as SerializableGraphQLErrorLike; + error.name = 'GraphQLError'; + if (isDev) { + const extensions = + originalError instanceof Error + ? { message: originalError.message, stack: originalError.stack } + : { message: String(originalError) }; -export const formatError: FormatErrorHandler = (err, message, isDev) => { - if (err instanceof GraphQLError) { - if ( - /** execution error */ - (err.originalError && err.originalError instanceof EnvelopError === false) || - /** validate and parse errors */ - (err.originalError === undefined && err instanceof EnvelopError === false) - ) { - return new GraphQLError( - message, - err.nodes, - err.source, - err.positions, - err.path, - undefined, - isDev - ? { - originalError: { - message: err.originalError?.message ?? err.message, - stack: err.originalError?.stack ?? err.stack, - }, - } - : undefined - ); - } - return err; + Object.defineProperty(error, 'extensions', { + get() { + return extensions; + }, + }); } - return new GraphQLError(message); -}; + + Object.defineProperty(error, 'toJSON', { + value() { + return { + message: error.message, + extensions: error.extensions, + }; + }, + }); + + return error as SerializableGraphQLErrorLike; +} + +export const createDefaultMaskError = + (isDev: boolean): MaskError => + (error, message) => { + if (isGraphQLError(error)) { + if (error?.originalError) { + if (isGraphQLError(error.originalError)) { + return error; + } + return createSerializableGraphQLError(message, error, isDev); + } + return error; + } + return createSerializableGraphQLError(message, error, isDev); + }; + +const isDev = globalThis.process?.env?.NODE_ENV === 'development'; + +export const defaultMaskError: MaskError = createDefaultMaskError(isDev); export type UseMaskedErrorsOpts = { - /** The function used for format/identify errors. */ - formatError?: FormatErrorHandler; + /** The function used for identify and mask errors. */ + maskError?: MaskError; /** The error message that shall be used for masked errors. */ errorMessage?: string; - /** - * Additional information that is forwarded to the `formatError` function. - * The default value is `process.env['NODE_ENV'] === 'development'` - */ - isDev?: boolean; - /** - * Whether parse errors should be processed by this plugin. - * In general it is not recommend to set this flag to `true` - * as a `parse` error contains useful information for debugging a GraphQL operation. - * A `parse` error never contains any sensitive information. - * @default false - */ - handleParseErrors?: boolean; - /** - * Whether validation errors should processed by this plugin. - * In general we recommend against setting this flag to `true` - * as a `validate` error contains useful information for debugging a GraphQL operation. - * A `validate` error contains "did you mean x" suggestions that make it easier - * to reverse-introspect a GraphQL schema whose introspection capabilities got disabled. - * Instead of disabling introspection and masking validation errors, using persisted operations - * is a safer solution for avoiding the execution of unwanted/arbitrary operations. - * @default false - */ - handleValidationErrors?: boolean; }; const makeHandleResult = - (format: FormatErrorHandler, message: string, isDev: boolean) => + (maskError: MaskError, message: string) => ({ result, setResult }: { result: ExecutionResult; setResult: (result: ExecutionResult) => void }) => { if (result.errors != null) { - setResult({ ...result, errors: result.errors.map(error => format(error, message, isDev)) }); + setResult({ ...result, errors: result.errors.map(error => maskError(error, message)) }); } }; export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => { - const format = opts?.formatError ?? formatError; + const maskError = opts?.maskError ?? defaultMaskError; const message = opts?.errorMessage || DEFAULT_ERROR_MESSAGE; - // eslint-disable-next-line dot-notation - const isDev = opts?.isDev ?? (typeof process !== 'undefined' ? process.env['NODE_ENV'] === 'development' : false); - const handleResult = makeHandleResult(format, message, isDev); + const handleResult = makeHandleResult(maskError, message); return { - onParse: - opts?.handleParseErrors === true - ? function onParse() { - return function onParseEnd({ result, replaceParseResult }) { - if (result instanceof Error) { - replaceParseResult(format(result, message, isDev)); - } - }; - } - : undefined, - onValidate: - opts?.handleValidationErrors === true - ? function onValidate() { - return function onValidateEnd({ valid, result, setResult }) { - if (valid === false) { - setResult(result.map(error => format(error, message, isDev))); - } - }; - } - : undefined, onPluginInit(context) { context.registerContextErrorHandler(({ error, setError }) => { - if (error instanceof GraphQLError === false && error instanceof Error) { - error = new GraphQLError(error.message, undefined, undefined, undefined, undefined, error); - } - setError(format(error, message, isDev)); + setError(maskError(error, message)); }); }, onExecute() { @@ -130,7 +105,7 @@ export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => { return handleStreamOrSingleExecutionResult(payload, handleResult); }, onSubscribeError({ error, setError }) { - setError(format(error, message, isDev)); + setError(maskError(error, message)); }, }; }, diff --git a/packages/core/src/plugins/use-payload-formatter.ts b/packages/core/src/plugins/use-payload-formatter.ts index 55ebcb77ef..ea2f6b8d91 100644 --- a/packages/core/src/plugins/use-payload-formatter.ts +++ b/packages/core/src/plugins/use-payload-formatter.ts @@ -1,6 +1,5 @@ -import { Plugin, TypedExecutionArgs } from '@envelop/types'; +import { Plugin, TypedExecutionArgs, ExecutionResult } from '@envelop/types'; import { handleStreamOrSingleExecutionResult } from '../utils.js'; -import { ExecutionResult } from 'graphql'; export type FormatterFunction = ( result: ExecutionResult, diff --git a/packages/core/src/plugins/use-schema.ts b/packages/core/src/plugins/use-schema.ts index 61b3259dec..f7246567d3 100644 --- a/packages/core/src/plugins/use-schema.ts +++ b/packages/core/src/plugins/use-schema.ts @@ -1,7 +1,6 @@ -import { GraphQLSchema } from 'graphql'; import { DefaultContext, Maybe, Plugin } from '@envelop/types'; -export const useSchema = (schema: GraphQLSchema): Plugin => { +export const useSchema = (schema: any): Plugin => { return { onPluginInit({ setSchema }) { setSchema(schema); @@ -9,20 +8,10 @@ export const useSchema = (schema: GraphQLSchema): Plugin => { }; }; -export const useLazyLoadedSchema = (schemaLoader: (context: Maybe) => GraphQLSchema): Plugin => { +export const useSchemaByContext = (schemaLoader: (context: Maybe) => any): Plugin => { return { onEnveloped({ setSchema, context }) { setSchema(schemaLoader(context)); }, }; }; - -export const useAsyncSchema = (schemaPromise: Promise): Plugin => { - return { - onPluginInit({ setSchema }) { - schemaPromise.then(schemaObj => { - setSchema(schemaObj); - }); - }, - }; -}; diff --git a/packages/core/src/plugins/use-timing.ts b/packages/core/src/plugins/use-timing.ts deleted file mode 100644 index 3e8886294e..0000000000 --- a/packages/core/src/plugins/use-timing.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* eslint-disable no-console */ -import { Plugin } from '@envelop/types'; -import { DocumentNode, ExecutionArgs, getOperationAST, GraphQLResolveInfo, Source, SubscriptionArgs } from 'graphql'; -import { isIntrospectionOperationString, envelopIsIntrospectionSymbol } from '../utils.js'; - -const HR_TO_NS = 1e9; -const NS_TO_MS = 1e6; - -export type ResultTiming = { ms: number; ns: number }; - -export type TimingPluginOptions = { - skipIntrospection?: boolean; - onContextBuildingMeasurement?: (timing: ResultTiming) => void; - onExecutionMeasurement?: (args: ExecutionArgs, timing: ResultTiming) => void; - onSubscriptionMeasurement?: (args: SubscriptionArgs, timing: ResultTiming) => void; - onParsingMeasurement?: (source: Source | string, timing: ResultTiming) => void; - onValidationMeasurement?: (document: DocumentNode, timing: ResultTiming) => void; - onResolverMeasurement?: (info: GraphQLResolveInfo, timing: ResultTiming) => void; -}; - -const DEFAULT_OPTIONS: TimingPluginOptions = { - onExecutionMeasurement: (args, timing) => - console.log(`Operation execution "${args.operationName}" done in ${timing.ms}ms`), - onSubscriptionMeasurement: (args, timing) => - console.log(`Operation subscription "${args.operationName}" done in ${timing.ms}ms`), - onParsingMeasurement: (source: Source | string, timing: ResultTiming) => - console.log(`Parsing "${source}" done in ${timing.ms}ms`), - onValidationMeasurement: (document: DocumentNode, timing: ResultTiming) => - console.log(`Validation "${getOperationAST(document)?.name?.value || '-'}" done in ${timing.ms}ms`), - onResolverMeasurement: (info: GraphQLResolveInfo, timing: ResultTiming) => - console.log(`\tResolver of "${info.parentType.toString()}.${info.fieldName}" done in ${timing.ms}ms`), - onContextBuildingMeasurement: (timing: ResultTiming) => console.log(`Context building done in ${timing.ms}ms`), -}; - -const deltaFrom = (hrtime: [number, number]): { ms: number; ns: number } => { - const delta = process.hrtime(hrtime); - const ns = delta[0] * HR_TO_NS + delta[1]; - - return { - ns, - get ms() { - return ns / NS_TO_MS; - }, - }; -}; - -type InternalPluginContext = { - [envelopIsIntrospectionSymbol]?: true; -}; - -export const useTiming = (rawOptions?: TimingPluginOptions): Plugin => { - const options = { - ...DEFAULT_OPTIONS, - ...rawOptions, - }; - - const result: Plugin = {}; - - if (options.onContextBuildingMeasurement) { - result.onContextBuilding = ({ context }) => { - if (context[envelopIsIntrospectionSymbol]) { - return; - } - - const contextStartTime = process.hrtime(); - - return () => { - options.onContextBuildingMeasurement!(deltaFrom(contextStartTime)); - }; - }; - } - - if (options.onParsingMeasurement) { - result.onParse = ({ params, extendContext }) => { - if (options.skipIntrospection && isIntrospectionOperationString(params.source)) { - extendContext({ - [envelopIsIntrospectionSymbol]: true, - }); - - return; - } - const parseStartTime = process.hrtime(); - - return () => { - options.onParsingMeasurement!(params.source, deltaFrom(parseStartTime)); - }; - }; - } - - if (options.onValidationMeasurement) { - result.onValidate = ({ params, context }) => { - if (context[envelopIsIntrospectionSymbol]) { - return; - } - - const validateStartTime = process.hrtime(); - - return () => { - options.onValidationMeasurement!(params.documentAST, deltaFrom(validateStartTime)); - }; - }; - } - - if (options.onExecutionMeasurement) { - if (options.onResolverMeasurement) { - result.onExecute = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const executeStartTime = process.hrtime(); - - return { - onExecuteDone: () => { - options.onExecutionMeasurement!(args, deltaFrom(executeStartTime)); - }, - }; - }; - - result.onResolverCalled = ({ info }) => { - const resolverStartTime = process.hrtime(); - - return () => { - options.onResolverMeasurement!(info, deltaFrom(resolverStartTime)); - }; - }; - } else { - result.onExecute = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const executeStartTime = process.hrtime(); - - return { - onExecuteDone: () => { - options.onExecutionMeasurement!(args, deltaFrom(executeStartTime)); - }, - }; - }; - } - } - - if (options.onSubscriptionMeasurement) { - if (options.onResolverMeasurement) { - result.onSubscribe = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const subscribeStartTime = process.hrtime(); - - return { - onSubscribeResult: () => { - options.onSubscriptionMeasurement && options.onSubscriptionMeasurement(args, deltaFrom(subscribeStartTime)); - }, - }; - }; - - result.onResolverCalled = ({ info }) => { - const resolverStartTime = process.hrtime(); - - return () => { - options.onResolverMeasurement && options.onResolverMeasurement(info, deltaFrom(resolverStartTime)); - }; - }; - } else { - result.onSubscribe = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const subscribeStartTime = process.hrtime(); - - return { - onSubscribeResult: () => { - options.onSubscriptionMeasurement && options.onSubscriptionMeasurement(args, deltaFrom(subscribeStartTime)); - }, - }; - }; - } - } - - return result; -}; diff --git a/packages/core/src/traced-orchestrator.ts b/packages/core/src/traced-orchestrator.ts deleted file mode 100644 index a3ca1b056d..0000000000 --- a/packages/core/src/traced-orchestrator.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { - DocumentNode, - ExecutionArgs, - GraphQLFieldResolver, - GraphQLSchema, - GraphQLTypeResolver, - SubscriptionArgs, -} from 'graphql'; -import { ArbitraryObject, Maybe } from '@envelop/types'; -import { EnvelopOrchestrator } from './orchestrator.js'; -import { isAsyncIterable } from './utils.js'; - -const getTimestamp = - typeof globalThis !== 'undefined' && globalThis?.performance?.now - ? () => globalThis.performance.now() - : () => Date.now(); - -const measure = () => { - const start = getTimestamp(); - return () => { - const end = getTimestamp(); - return end - start; - }; -}; - -const tracingSymbol = Symbol('envelopTracing'); - -export function traceOrchestrator( - orchestrator: EnvelopOrchestrator -): EnvelopOrchestrator { - const createTracer = (name: string, ctx: Record) => { - const end = measure(); - - return () => { - ctx[tracingSymbol][name] = end(); - }; - }; - - return { - ...orchestrator, - init: (ctx = {} as TInitialContext) => { - ctx![tracingSymbol] = ctx![tracingSymbol] || {}; - const done = createTracer('init', ctx || {}); - - try { - return orchestrator.init(ctx); - } finally { - done(); - } - }, - parse: (ctx = {} as TInitialContext) => { - ctx[tracingSymbol] = ctx[tracingSymbol] || {}; - const actualFn = orchestrator.parse(ctx); - - return (...args) => { - const done = createTracer('parse', ctx); - - try { - return actualFn(...args); - } finally { - done(); - } - }; - }, - validate: (ctx = {} as TInitialContext) => { - ctx[tracingSymbol] = ctx[tracingSymbol] || {}; - const actualFn = orchestrator.validate(ctx); - - return (...args) => { - const done = createTracer('validate', ctx); - - try { - return actualFn(...args); - } finally { - done(); - } - }; - }, - execute: async ( - argsOrSchema: ExecutionArgs | GraphQLSchema, - document?: DocumentNode, - rootValue?: any, - contextValue?: any, - variableValues?: Maybe<{ [key: string]: any }>, - operationName?: Maybe, - fieldResolver?: Maybe>, - typeResolver?: Maybe> - ) => { - const args: ExecutionArgs = - argsOrSchema instanceof GraphQLSchema - ? { - schema: argsOrSchema, - document: document!, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - } - : argsOrSchema; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore GraphQL.js types contextValue as unknown - const done = createTracer('execute', args.contextValue || {}); - - try { - const result = await orchestrator.execute(args); - done(); - - if (!isAsyncIterable(result)) { - result.extensions = result.extensions || {}; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore GraphQL.js types contextValue as unknown - result.extensions.envelopTracing = args.contextValue[tracingSymbol]; - } else { - // eslint-disable-next-line no-console - console.warn( - `"traceOrchestrator" encountered a AsyncIterator which is not supported yet, so tracing data is not available for the operation.` - ); - } - - return result; - } catch (e) { - done(); - - throw e; - } - }, - subscribe: async ( - argsOrSchema: SubscriptionArgs | GraphQLSchema, - document?: DocumentNode, - rootValue?: any, - contextValue?: any, - variableValues?: Maybe<{ [key: string]: any }>, - operationName?: Maybe, - fieldResolver?: Maybe>, - subscribeFieldResolver?: Maybe> - ) => { - const args: SubscriptionArgs = - argsOrSchema instanceof GraphQLSchema - ? { - schema: argsOrSchema, - document: document!, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - subscribeFieldResolver, - } - : argsOrSchema; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore GraphQL.js types contextValue as unknown - const done = createTracer('subscribe', args.contextValue || {}); - - try { - return await orchestrator.subscribe(args); - } finally { - done(); - } - }, - contextFactory: (ctx = {} as TInitialContext) => { - const actualFn = orchestrator.contextFactory(ctx); - - return async childCtx => { - const done = createTracer('contextFactory', ctx); - - try { - return await actualFn(childCtx); - } finally { - done(); - } - }; - }, - }; -} diff --git a/packages/core/src/traced-schema.ts b/packages/core/src/traced-schema.ts deleted file mode 100644 index 9de218dadd..0000000000 --- a/packages/core/src/traced-schema.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; -import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; - -export const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); -export const resolversHooksSymbol = Symbol('RESOLVERS_HOOKS'); - -export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { - if (!schema || schema[trackedSchemaSymbol]) { - return; - } - - schema[trackedSchemaSymbol] = true; - const entries = Object.values(schema.getTypeMap()); - - for (const type of entries) { - if (!isIntrospectionType(type) && isObjectType(type)) { - const fields = Object.values(type.getFields()); - - for (const field of fields) { - let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; - - field.resolve = async (root, args, context, info) => { - if (context && context[resolversHooksSymbol]) { - const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; - const afterCalls: AfterResolverHook[] = []; - - for (const hook of hooks) { - const afterFn = await hook({ - root, - args, - context, - info, - resolverFn, - replaceResolverFn: newFn => { - resolverFn = newFn as ResolverFn; - }, - }); - afterFn && afterCalls.push(afterFn); - } - - try { - let result = await resolverFn(root, args, context, info); - - for (const afterFn of afterCalls) { - afterFn({ - result, - setResult: newResult => { - result = newResult; - }, - }); - } - - return result; - } catch (e) { - let resultErr = e; - - for (const afterFn of afterCalls) { - afterFn({ - result: resultErr, - setResult: newResult => { - resultErr = newResult; - }, - }); - } - - throw resultErr; - } - } else { - return resolverFn(root, args, context, info); - } - }; - } - } - } -} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index b99bf574a2..8126e59a0d 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,15 +1,3 @@ -import { - ASTNode, - DocumentNode, - Kind, - OperationDefinitionNode, - visit, - BREAK, - Source, - ExecutionResult, - SubscriptionArgs, - ExecutionArgs, -} from 'graphql'; import { AsyncIterableIteratorOrValue, ExecuteFunction, @@ -21,38 +9,16 @@ import { OnExecuteDoneEventPayload, OnExecuteDoneHookResult, OnExecuteDoneHookResultOnNextHook, + ExecutionArgs, } from '@envelop/types'; export const envelopIsIntrospectionSymbol = Symbol('ENVELOP_IS_INTROSPECTION'); -export function isOperationDefinition(def: ASTNode): def is OperationDefinitionNode { - return def.kind === Kind.OPERATION_DEFINITION; -} - -export function isIntrospectionOperation(operation: OperationDefinitionNode): boolean { - return isIntrospectionDocument({ - kind: Kind.DOCUMENT, - definitions: [operation], - }); -} -export function isIntrospectionDocument(document: DocumentNode): boolean { - let isIntrospectionOperation = false; - visit(document, { - Field: node => { - if (node.name.value === '__schema' || node.name.value === '__type') { - isIntrospectionOperation = true; - return BREAK; - } - }, - }); - return isIntrospectionOperation; -} - -export function isIntrospectionOperationString(operation: string | Source): boolean { +export function isIntrospectionOperationString(operation: string | any): boolean { return (typeof operation === 'string' ? operation : operation.body).indexOf('__schema') !== -1; } -function getSubscribeArgs(args: PolymorphicSubscribeArguments): SubscriptionArgs { +function getSubscribeArgs(args: PolymorphicSubscribeArguments): ExecutionArgs { return args.length === 1 ? args[0] : { @@ -70,10 +36,8 @@ function getSubscribeArgs(args: PolymorphicSubscribeArguments): SubscriptionArgs /** * Utility function for making a subscribe function that handles polymorphic arguments. */ -export const makeSubscribe = ( - subscribeFn: (args: SubscriptionArgs) => PromiseOrValue> -): SubscribeFunction => - ((...polyArgs: PolymorphicSubscribeArguments): PromiseOrValue> => +export const makeSubscribe = (subscribeFn: (args: ExecutionArgs) => any): SubscribeFunction => + ((...polyArgs: PolymorphicSubscribeArguments): PromiseOrValue> => subscribeFn(getSubscribeArgs(polyArgs))) as SubscribeFunction; export function mapAsyncIterator( @@ -142,9 +106,9 @@ function getExecuteArgs(args: PolymorphicExecuteArguments): ExecutionArgs { * Utility function for making a execute function that handles polymorphic arguments. */ export const makeExecute = ( - executeFn: (args: ExecutionArgs) => PromiseOrValue> + executeFn: (args: ExecutionArgs) => PromiseOrValue> ): ExecuteFunction => - ((...polyArgs: PolymorphicExecuteArguments): PromiseOrValue> => + ((...polyArgs: PolymorphicExecuteArguments): PromiseOrValue> => executeFn(getExecuteArgs(polyArgs))) as unknown as ExecuteFunction; /** @@ -255,3 +219,15 @@ export function errorAsyncIterator( return stream; } + +export function isSubscriptionOperation(document: any, operationName?: string): boolean { + if (operationName) { + return document.definitions.some( + (def: any) => + def.kind === 'OperationDefinition' && def.name?.value === operationName && def.operation === 'subscription' + ); + } + return document.definitions.some( + (def: any) => def.kind === 'OperationDefinition' && def.operation === 'subscription' + ); +} diff --git a/packages/core/test/context.spec.ts b/packages/core/test/context.spec.ts index 77fae79514..57d3179895 100644 --- a/packages/core/test/context.spec.ts +++ b/packages/core/test/context.spec.ts @@ -1,4 +1,4 @@ -import { ContextFactoryFn, EnvelopError, useExtendContext } from '@envelop/core'; +import { ContextFactoryFn, useExtendContext } from '@envelop/core'; import { createSpiedPlugin, createTestkit } from '@envelop/testing'; import { schema, query } from './common.js'; @@ -6,7 +6,7 @@ describe('contextFactory', () => { it('Should call before parse and after parse correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query); + await teskit.perform({ query }); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledWith({ context: expect.any(Object), @@ -24,7 +24,7 @@ describe('contextFactory', () => { it('Should set initial `createProxy` arguments as initial context', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query, {}, { test: true }); + await teskit.perform({ query }, { test: true }); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledWith({ context: expect.objectContaining({ @@ -55,7 +55,7 @@ describe('contextFactory', () => { schema ); - await teskit.execute(query, {}, {}); + await teskit.perform({ query }); expect(afterContextSpy).toHaveBeenCalledWith({ context: expect.objectContaining({ test: true, @@ -94,7 +94,7 @@ describe('contextFactory', () => { ], schema ); - await teskit.execute(query, {}, {}); + await teskit.perform({ query }); expect(afterContextSpy).toHaveBeenCalledWith( expect.objectContaining({ context: expect.objectContaining({ @@ -122,7 +122,7 @@ describe('contextFactory', () => { }; const throwingContextFactory: ContextFactoryFn = () => { - throw new EnvelopError('The server was about to step on a turtle'); + throw new Error('The server was about to step on a turtle'); }; const teskit = createTestkit( @@ -140,7 +140,7 @@ describe('contextFactory', () => { schema ); - const execution = teskit.execute(query, {}, { test: true }); + const execution = teskit.perform({ query, variables: {} }, { test: true }); return new Promise((resolve, reject) => { if (execution instanceof Promise) { return execution.then().catch(() => { @@ -155,7 +155,7 @@ describe('contextFactory', () => { test: true, variables: expect.any(Object), }), - error: new EnvelopError('The server was about to step on a turtle'), + error: new Error('The server was about to step on a turtle'), setError: expect.any(Function), }) ); @@ -165,7 +165,7 @@ describe('contextFactory', () => { return resolve(); }); } else { - return reject('Expected result of testkit.execute to return a promise'); + return reject('Expected result of testkit.perform to return a promise'); } }); }); diff --git a/packages/core/test/execute.spec.ts b/packages/core/test/execute.spec.ts index f8ac840845..79cedc03f3 100644 --- a/packages/core/test/execute.spec.ts +++ b/packages/core/test/execute.spec.ts @@ -49,9 +49,8 @@ describe('execute', () => { it('Should wrap and trigger events correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query, {}, { test: 1 }); + await teskit.perform({ query, variables: {} }, { test: 1 }); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledTimes(1); - expect(spiedPlugin.spies.beforeResolver).toHaveBeenCalledTimes(3); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledWith({ executeFn: expect.any(Function), setExecuteFn: expect.any(Function), @@ -59,7 +58,6 @@ describe('execute', () => { setResultAndStopExecution: expect.any(Function), args: { contextValue: expect.objectContaining({ test: 1 }), - rootValue: {}, schema: expect.any(GraphQLSchema), operationName: undefined, fieldResolver: undefined, @@ -71,7 +69,6 @@ describe('execute', () => { }, }); - expect(spiedPlugin.spies.afterResolver).toHaveBeenCalledTimes(3); expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledWith({ args: expect.any(Object), @@ -99,7 +96,7 @@ describe('execute', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(altExecute).toHaveBeenCalledTimes(1); }); @@ -115,7 +112,7 @@ describe('execute', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); }); describe('setResultAndStopExecution', () => { @@ -149,7 +146,7 @@ describe('execute', () => { ], schema ); - const result = await teskit.execute(query); + const result = await teskit.perform({ query }); assertSingleExecutionValue(result); expect(onExecuteCalled).toEqual(true); expect(onExecuteDoneCalled).toEqual(true); @@ -194,7 +191,7 @@ describe('execute', () => { ], schema ); - const result = await teskit.execute(query); + const result = await teskit.perform({ query }); assertSingleExecutionValue(result); expect(onExecuteCalled).toEqual(false); expect(onExecuteDoneCalled).toEqual(false); @@ -210,67 +207,6 @@ describe('execute', () => { }); }); - it('Should allow to register to before and after resolver calls', async () => { - const afterResolver = jest.fn(); - const onResolverCalled = jest.fn(() => afterResolver); - - const teskit = createTestkit( - [ - { - onResolverCalled, - }, - ], - schema - ); - - await teskit.execute(query); - expect(onResolverCalled).toHaveBeenCalledTimes(3); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: {}, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'me', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'id', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'name', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - - expect(afterResolver).toHaveBeenCalledTimes(3); - expect(afterResolver).toHaveBeenCalledWith({ - result: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - setResult: expect.any(Function), - }); - expect(afterResolver).toHaveBeenCalledWith({ - result: 1, - setResult: expect.any(Function), - }); - expect(afterResolver).toHaveBeenCalledWith({ - result: 'Dotan Simha', - setResult: expect.any(Function), - }); - }); - it('Should be able to manipulate streams', async () => { const streamExecuteFn = async function* () { for (const value of ['a', 'b', 'c', 'd']) { @@ -299,11 +235,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); const values = await collectAsyncIteratorValues(result); expect(values).toEqual([ @@ -347,11 +285,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); // run AsyncGenerator await collectAsyncIteratorValues(result); @@ -406,11 +346,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); const iterator = result[Symbol.asyncIterator](); await iterator.next(); @@ -468,11 +410,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); const iterator = result[Symbol.asyncIterator](); const nextPromise = iterator.next(); @@ -507,7 +451,7 @@ describe('execute', () => { schema ); - expect(await testkit.execute(query)).toEqual({ data: { test: 'test' } }); + expect(await testkit.perform({ query })).toEqual({ data: { test: 'test' } }); }); it('hook into subscription phases with proper cleanup on the source', async () => { @@ -583,7 +527,7 @@ describe('execute', () => { } `; - const result = await testkit.execute(document); + const result = await testkit.perform({ query: document }); assertStreamExecutionValue(result); await result.next(); await result.next(); @@ -660,11 +604,13 @@ it.each([ schema ); - const result = await teskit.execute(/* GraphQL */ ` - subscription { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + subscription { + alphabet + } + `, + }); assertStreamExecutionValue(result); const iterator = result[Symbol.asyncIterator](); const nextPromise = iterator.next(); diff --git a/packages/core/test/extends.spec.ts b/packages/core/test/extends.spec.ts index eebb8099d6..3380e0d298 100644 --- a/packages/core/test/extends.spec.ts +++ b/packages/core/test/extends.spec.ts @@ -2,6 +2,7 @@ import { createSpiedPlugin, createTestkit } from '@envelop/testing'; import { envelop, useExtendContext, useLogger, useSchema } from '../src/index.js'; import { useEnvelop } from '../src/plugins/use-envelop.js'; import { schema, query } from './common.js'; +import { parse, execute, validate, subscribe } from 'graphql'; describe('extending envelops', () => { it('should allow to extend envelops', async () => { @@ -9,6 +10,10 @@ describe('extending envelops', () => { const baseEnvelop = envelop({ plugins: [useLogger(), spiedPlugin.plugin], + parse, + execute, + validate, + subscribe, }); const onExecuteChildSpy = jest.fn(); @@ -21,10 +26,14 @@ describe('extending envelops', () => { onExecute: onExecuteChildSpy, }, ], + parse, + execute, + validate, + subscribe, }); const teskit = createTestkit(instance); - await teskit.execute(query, {}); + await teskit.perform({ query }); expect(onExecuteChildSpy).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledTimes(1); diff --git a/packages/core/test/parse.spec.ts b/packages/core/test/parse.spec.ts index d0c7d85624..405f445de3 100644 --- a/packages/core/test/parse.spec.ts +++ b/packages/core/test/parse.spec.ts @@ -6,7 +6,7 @@ describe('parse', () => { it('Should call before parse and after parse correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query); + await teskit.perform({ query }); expect(spiedPlugin.spies.beforeParse).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeParse).toHaveBeenCalledWith({ context: expect.any(Object), @@ -41,7 +41,7 @@ describe('parse', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(1); expect(replacementFn).toHaveBeenCalledWith(query, undefined); }); @@ -63,7 +63,7 @@ describe('parse', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(0); expect(afterFn).toHaveBeenCalledTimes(1); expect(afterFn).toHaveBeenCalledWith({ @@ -103,7 +103,7 @@ describe('parse', () => { ], schema ); - const result = await teskit.execute(query); + const result = await teskit.perform({ query }); assertSingleExecutionValue(result); expect(afterFn).toHaveBeenCalledTimes(1); expect(result.data?.currentUser).toBeDefined(); diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts new file mode 100644 index 0000000000..c3346452d7 --- /dev/null +++ b/packages/core/test/perform.spec.ts @@ -0,0 +1,302 @@ +import { parse, validate, execute, subscribe, GraphQLError } from 'graphql'; +import { envelop, OnPerformDoneHook, OnPerformHook, useSchema } from '../src/index.js'; +import { assertSingleExecutionValue, assertStreamExecutionValue } from '@envelop/testing'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +const graphqlFuncs = { parse, validate, execute, subscribe }; + +const greetings = ['Hello', 'Bonjour', 'Ciao']; +const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String! + } + type Subscription { + greetings: String! + } + `, + resolvers: { + Query: { + hello() { + return 'world'; + }, + }, + Subscription: { + greetings: { + async *subscribe() { + for (const greet of greetings) { + yield { greetings: greet }; + } + }, + }, + }, + }, +}); + +describe('perform', () => { + it('should parse, validate, assemble context and execute', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "world", + }, + } + `); + }); + + it('should parse, validate, assemble context and subscribe', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: 'subscription { greetings }' }); + assertStreamExecutionValue(result); + + let i = 0; + for await (const part of result) { + expect(part).toEqual({ + data: { + greetings: greetings[i], + }, + }); + i++; + } + }); + + it('should include parsing GraphQL errors in result', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Syntax Error: Expected Name, found .], + ], + } + `); + }); + + it('should throw parsing non-GraphQL errors', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + parse: () => { + throw new Error('Oops!'); + }, + plugins: [useSchema(schema)], + }); + + const { perform } = getEnveloped(); + + expect(perform({ query: '{' })).rejects.toBeInstanceOf(Error); + }); + + it('should include validation GraphQL errors in result', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{ idontexist }' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Cannot query field "idontexist" on type "Query".], + ], + } + `); + }); + + it('should throw validation non-GraphQL errors', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + validate: () => { + throw new Error('Oops!'); + }, + plugins: [useSchema(schema)], + }); + + const { perform } = getEnveloped(); + + expect(perform({ query: '{ idontexist }' })).rejects.toBeInstanceOf(Error); + }); + + it('should include thrown validation errors in result', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onValidate: ({ addValidationRule }) => { + addValidationRule(() => { + throw new GraphQLError('Invalid!'); + }); + }, + }, + ], + }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Invalid!], + ], + } + `); + }); + + it('should invoke onPerform plugin hooks', async () => { + const onPerformDoneFn = jest.fn((() => { + // noop + }) as OnPerformDoneHook); + const onPerformFn = jest.fn((() => ({ + onPerformDone: onPerformDoneFn, + })) as OnPerformHook); + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: onPerformFn, + }, + ], + }); + + const params = { query: '{ hello }' }; + const { perform } = getEnveloped({ initial: 'context' }); + await perform(params, { extension: 'context' }); + + expect(onPerformFn).toBeCalled(); + expect(onPerformFn.mock.calls[0][0].context).toEqual({ initial: 'context' }); + expect(onPerformFn.mock.calls[0][0].params).toBe(params); + + expect(onPerformDoneFn).toBeCalled(); + expect(onPerformDoneFn.mock.calls[0][0].context).toEqual({ initial: 'context', extension: 'context' }); + expect(onPerformDoneFn.mock.calls[0][0].result).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "world", + }, + } + `); + }); + + it('should replace params in onPerform plugin', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: ({ setParams }) => { + setParams({ query: '{ hello }' }); + }, + }, + ], + }); + + const { perform } = getEnveloped(); + const result = await perform({ query: 'subscribe { greetings }' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "world", + }, + } + `); + }); + + it('should replace result in onPerformDone plugin', async () => { + const replacedResult = { data: { something: 'else' } }; + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: () => ({ + onPerformDone: ({ setResult }) => { + setResult(replacedResult); + }, + }), + }, + ], + }); + + const { perform } = getEnveloped(); + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result).toBe(replacedResult); + }); + + it('should early result in onPerform plugin', async () => { + const earlyResult = { data: { hi: 'hello' } }; + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: ({ setResult }) => { + setResult(earlyResult); + }, + }, + ], + }); + + const { perform } = getEnveloped(); + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result).toBe(earlyResult); + }); + + it('should provide result with parsing errors to onPerformDone hook', async () => { + const onPerformDoneFn = jest.fn((() => { + // noop + }) as OnPerformDoneHook); + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: () => ({ + onPerformDone: onPerformDoneFn, + }), + }, + ], + }); + + const { perform } = getEnveloped(); + await perform({ query: '{' }); + + expect(onPerformDoneFn).toBeCalled(); + expect(onPerformDoneFn.mock.calls[0][0].result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Syntax Error: Expected Name, found .], + ], + } + `); + }); +}); diff --git a/packages/core/test/plugins/use-error-handler.spec.ts b/packages/core/test/plugins/use-error-handler.spec.ts index 77b4c73e71..2b73af42c0 100644 --- a/packages/core/test/plugins/use-error-handler.spec.ts +++ b/packages/core/test/plugins/use-error-handler.spec.ts @@ -1,7 +1,11 @@ import { useErrorHandler } from '../../src/plugins/use-error-handler.js'; import { assertStreamExecutionValue, collectAsyncIteratorValues, createTestkit } from '@envelop/testing'; +import { Plugin } from '@envelop/types'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { Repeater } from '@repeaterjs/repeater'; +import { createGraphQLError } from '@graphql-tools/utils'; +import { schema } from '../common.js'; +import { useExtendContext } from '@envelop/core'; describe('useErrorHandler', () => { it('should invoke error handler when error happens during execution', async () => { @@ -24,18 +28,69 @@ describe('useErrorHandler', () => { const mockHandler = jest.fn(); const testInstance = createTestkit([useErrorHandler(mockHandler)], schema); - await testInstance.execute(`query { foo }`, {}, { foo: 'bar' }); + await testInstance.perform({ query: `query { foo }` }, { foo: 'bar' }); + expect(mockHandler).toHaveBeenCalledWith(expect.objectContaining({ phase: 'execution' })); + }); + + it('should invoke error handler when error happens during parse', async () => { + expect.assertions(2); + const mockHandler = jest.fn(); + const testInstance = createTestkit([useErrorHandler(mockHandler)], schema); + await testInstance.execute(`query { me `, {}); + expect(mockHandler).toHaveBeenCalledTimes(1); expect(mockHandler).toHaveBeenCalledWith( - [testError], expect.objectContaining({ - contextValue: expect.objectContaining({ - foo: 'bar', - }), + phase: 'parse', + }) + ); + }); + + it('should invoke error handler on validation error', async () => { + expect.assertions(2); + const useMyFailingValidator: Plugin = { + onValidate(payload) { + payload.setValidationFn(() => { + return [createGraphQLError('Failure!')]; + }); + }, + }; + const mockHandler = jest.fn(); + const testInstance = createTestkit([useMyFailingValidator, useErrorHandler(mockHandler)], schema); + await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith( + expect.objectContaining({ + phase: 'validate', }) ); }); + it('should invoke error handle for context errors', async () => { + expect.assertions(2); + const mockHandler = jest.fn(); + const testInstance = createTestkit( + [ + useExtendContext((): {} => { + throw new Error('No context for you!'); + }), + useErrorHandler(mockHandler), + ], + schema + ); + + try { + await testInstance.execute(`query { me { name } }`); + } catch { + expect(mockHandler).toHaveBeenCalledWith( + expect.objectContaining({ + phase: 'context', + }) + ); + expect(mockHandler).toHaveBeenCalledTimes(1); + } + }); + it('should invoke error handler when error happens during subscription resolver call', async () => { const testError = new Error('Foobar'); @@ -66,16 +121,14 @@ describe('useErrorHandler', () => { const mockHandler = jest.fn(); const testInstance = createTestkit([useErrorHandler(mockHandler)], schema); - const result = await testInstance.execute(`subscription { foo }`, {}, { foo: 'bar' }); + const result = await testInstance.perform({ query: `subscription { foo }` }, { foo: 'bar' }); assertStreamExecutionValue(result); await collectAsyncIteratorValues(result); expect(mockHandler).toHaveBeenCalledWith( - [testError], expect.objectContaining({ - contextValue: expect.objectContaining({ - foo: 'bar', - }), + errors: expect.objectContaining([testError]), + phase: 'execution', }) ); }); diff --git a/packages/core/test/plugins/use-masked-errors.spec.ts b/packages/core/test/plugins/use-masked-errors.spec.ts index dd2e9774d3..2e8bd441e2 100644 --- a/packages/core/test/plugins/use-masked-errors.spec.ts +++ b/packages/core/test/plugins/use-masked-errors.spec.ts @@ -6,15 +6,15 @@ import { createTestkit, } from '@envelop/testing'; import { - EnvelopError, useMaskedErrors, DEFAULT_ERROR_MESSAGE, - formatError, - FormatErrorHandler, + MaskError, + createDefaultMaskError, } from '../../src/plugins/use-masked-errors.js'; -import { Plugin, useExtendContext } from '@envelop/core'; +import { useExtendContext } from '@envelop/core'; import { useAuth0 } from '../../../plugins/auth0/src/index.js'; import { GraphQLError } from 'graphql'; +import { createGraphQLError } from '@graphql-tools/utils'; describe('useMaskedErrors', () => { const schema = makeExecutableSchema({ @@ -28,9 +28,9 @@ describe('useMaskedErrors', () => { instantError: String streamError: String streamResolveError: String - instantEnvelopError: String - streamEnvelopError: String - streamResolveEnvelopError: String + instantGraphQLError: String + streamGraphQLError: String + streamResolveGraphQLError: String } `, resolvers: { @@ -39,12 +39,14 @@ describe('useMaskedErrors', () => { throw new Error('Secret sauce that should not leak.'); }, secretEnvelop: () => { - throw new EnvelopError('This message goes to all the clients out there!', { foo: 1 }); + throw createGraphQLError('This message goes to all the clients out there!', { extensions: { foo: 1 } }); }, secretWithExtensions: () => { - throw new EnvelopError('This message goes to all the clients out there!', { - code: 'Foo', - message: 'Bar', + throw createGraphQLError('This message goes to all the clients out there!', { + extensions: { + code: 'Foo', + message: 'Bar', + }, }); }, }, @@ -69,33 +71,33 @@ describe('useMaskedErrors', () => { throw new Error('Noop'); }, }, - instantEnvelopError: { + instantGraphQLError: { subscribe: async function () { - throw new EnvelopError('Noop'); + throw createGraphQLError('Noop'); }, resolve: _ => _, }, - streamEnvelopError: { + streamGraphQLError: { subscribe: async function* () { - throw new EnvelopError('Noop'); + throw createGraphQLError('Noop'); }, resolve: _ => _, }, - streamResolveEnvelopError: { + streamResolveGraphQLError: { subscribe: async function* () { yield '1'; }, resolve: _ => { - throw new EnvelopError('Noop'); + throw createGraphQLError('Noop'); }, }, }, }, }); - it('Should mask non EnvelopErrors', async () => { + it('Should mask non GraphQLErrors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secret }`); + const result = await testInstance.perform({ query: `query { secret }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -105,7 +107,7 @@ describe('useMaskedErrors', () => { it('Should not mask expected errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secretEnvelop }`); + const result = await testInstance.perform({ query: `query { secretEnvelop }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -114,34 +116,9 @@ describe('useMaskedErrors', () => { expect(error.extensions).toEqual({ foo: 1 }); }); - it('Should include the original error within the error extensions when `isDev` is set to `true`', async () => { - const testInstance = createTestkit([useMaskedErrors({ isDev: true })], schema); - const result = await testInstance.execute(`query { secret }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.extensions).toEqual({ - originalError: { - message: 'Secret sauce that should not leak.', - stack: expect.stringContaining('Error: Secret sauce that should not leak.'), - }, - }); - }); - - it('Should not include the original error within the error extensions when `isDev` is set to `false`', async () => { - const testInstance = createTestkit([useMaskedErrors({ isDev: false })], schema); - const result = await testInstance.execute(`query { secret }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.extensions).toEqual({}); - }); - it('Should not mask GraphQL operation syntax errors (of course it does not since we are only hooking in after execute, but just to be sure)', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { idonotexist }`); + const result = await testInstance.perform({ query: `query { idonotexist }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -149,9 +126,9 @@ describe('useMaskedErrors', () => { expect(error.message).toEqual('Cannot query field "idonotexist" on type "Query".'); }); - it('Should forward extensions from EnvelopError to final GraphQLError in errors array', async () => { + it('Should forward extensions from GraphQLError to final GraphQLError in errors array', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secretWithExtensions }`); + const result = await testInstance.perform({ query: `query { secretWithExtensions }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -160,6 +137,9 @@ describe('useMaskedErrors', () => { code: 'Foo', message: 'Bar', }); + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"errors\\":[{\\"message\\":\\"This message goes to all the clients out there!\\",\\"locations\\":[{\\"line\\":1,\\"column\\":9}],\\"path\\":[\\"secretWithExtensions\\"],\\"extensions\\":{\\"code\\":\\"Foo\\",\\"message\\":\\"Bar\\"}}],\\"data\\":null}"` + ); }); it('Should properly mask context creation errors with a custom error message', async () => { @@ -174,7 +154,7 @@ describe('useMaskedErrors', () => { schema ); try { - await testInstance.execute(`query { secretWithExtensions }`); + await testInstance.perform({ query: `query { secretWithExtensions }` }); } catch (err) { expect(err).toMatchInlineSnapshot(`[GraphQLError: My Custom Error Message.]`); } @@ -191,7 +171,7 @@ describe('useMaskedErrors', () => { schema ); try { - await testInstance.execute(`query { secretWithExtensions }`); + await testInstance.perform({ query: `query { secretWithExtensions }` }); } catch (err: any) { expect(err.message).toEqual(DEFAULT_ERROR_MESSAGE); } @@ -202,16 +182,16 @@ describe('useMaskedErrors', () => { const testInstance = createTestkit( [ useExtendContext((): {} => { - throw new EnvelopError('No context for you!', { foo: 1 }); + throw createGraphQLError('No context for you!', { extensions: { foo: 1 } }); }), useMaskedErrors(), ], schema ); try { - await testInstance.execute(`query { secretWithExtensions }`); + await testInstance.perform({ query: `query { secretWithExtensions }` }); } catch (err) { - if (err instanceof EnvelopError) { + if (err instanceof GraphQLError) { expect(err.message).toEqual(`No context for you!`); expect(err.extensions).toEqual({ foo: 1 }); } else { @@ -219,33 +199,10 @@ describe('useMaskedErrors', () => { } } }); - it('Should include the original context error in extensions in dev mode for error thrown during context creation.', async () => { - expect.assertions(3); - const testInstance = createTestkit( - [ - useExtendContext((): {} => { - throw new Error('No context for you!'); - }), - useMaskedErrors({ isDev: true }), - ], - schema - ); - try { - await testInstance.execute(`query { secretWithExtensions }`); - } catch (err: any) { - expect(err).toBeInstanceOf(GraphQLError); - expect(err.message).toEqual('Unexpected error.'); - expect(err.extensions).toEqual({ - originalError: { - message: 'No context for you!', - stack: expect.stringContaining('Error: No context for you!'), - }, - }); - } - }); + it('Should mask subscribe (sync/promise) subscription errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`subscription { instantError }`); + const result = await testInstance.perform({ query: `subscription { instantError }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -258,7 +215,7 @@ describe('useMaskedErrors', () => { [useMaskedErrors({ errorMessage: 'My Custom subscription error message.' })], schema ); - const result = await testInstance.execute(`subscription { instantError }`); + const result = await testInstance.perform({ query: `subscription { instantError }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toMatchInlineSnapshot(` @@ -268,9 +225,9 @@ describe('useMaskedErrors', () => { `); }); - it('Should not mask subscribe (sync/promise) subscription envelop errors', async () => { + it('Should not mask subscribe (sync/promise) subscription GraphQL errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`subscription { instantEnvelopError }`); + const result = await testInstance.perform({ query: `subscription { instantGraphQLError }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toMatchInlineSnapshot(` @@ -283,7 +240,7 @@ describe('useMaskedErrors', () => { it('Should mask subscribe (AsyncIterable) subscription errors', async () => { expect.assertions(1); const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -298,7 +255,7 @@ describe('useMaskedErrors', () => { [useMaskedErrors({ errorMessage: 'My AsyncIterable Custom Error Message.' })], schema ); - const resultStream = await testInstance.execute(`subscription { streamError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -309,7 +266,7 @@ describe('useMaskedErrors', () => { it('Should not mask subscribe (AsyncIterable) subscription envelop errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamEnvelopError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamGraphQLError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -320,7 +277,7 @@ describe('useMaskedErrors', () => { it('Should mask resolve subscription errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamResolveError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamResolveError }` }); assertStreamExecutionValue(resultStream); const allResults = await collectAsyncIteratorValues(resultStream); expect(allResults).toHaveLength(1); @@ -330,27 +287,25 @@ describe('useMaskedErrors', () => { const [error] = result.errors!; expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); }); + it('Should mask resolve subscription errors with a custom error message', async () => { const testInstance = createTestkit( [useMaskedErrors({ errorMessage: 'Custom resolve subscription errors.' })], schema ); - const resultStream = await testInstance.execute(`subscription { streamResolveError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamResolveError }` }); assertStreamExecutionValue(resultStream); const allResults = await collectAsyncIteratorValues(resultStream); expect(allResults).toHaveLength(1); const [result] = allResults; - expect(result.errors).toBeDefined(); - expect(result.errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Custom resolve subscription errors.], - ] - `); + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"errors\\":[{\\"message\\":\\"Custom resolve subscription errors.\\"}],\\"data\\":{\\"streamResolveError\\":null}}"` + ); }); it('Should not mask resolve subscription envelop errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamResolveEnvelopError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamResolveGraphQLError }` }); assertStreamExecutionValue(resultStream); const allResults = await collectAsyncIteratorValues(resultStream); expect(allResults).toHaveLength(1); @@ -374,22 +329,24 @@ describe('useMaskedErrors', () => { tokenType: 'Bearer', }; const testInstance = createTestkit([useMaskedErrors(), useAuth0(auto0Options)], schema); - try { - await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something' } } }); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: Invalid value provided for header "authorization"!]`); - } + let result = await testInstance.perform( + { query: `query { secret }` }, + { request: { headers: { authorization: 'Something' } } } + ); + assertSingleExecutionValue(result); + expect(result.errors?.[0]).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); - try { - await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something else' } } }); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: Unsupported token type provided: "Something"!]`); - } + result = await testInstance.perform( + { query: `query { secret }` }, + { request: { headers: { authorization: 'Something else' } } } + ); + assertSingleExecutionValue(result); + expect(result.errors?.[0]).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); }); it('should not mask parse errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { a `, {}); + const result = await testInstance.perform({ query: `query { a ` }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { @@ -399,17 +356,10 @@ describe('useMaskedErrors', () => { } `); }); - it('should mask parse errors with handleParseErrors option', async () => { - const testInstance = createTestkit([useMaskedErrors({ handleParseErrors: true })], schema); - const result = await testInstance.execute(`query { a `, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); - }); + it('should not mask validation errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); + const result = await testInstance.perform({ query: `query { iDoNotExistsMyGuy }` }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { @@ -419,22 +369,14 @@ describe('useMaskedErrors', () => { } `); }); - it('should mask validation errors with handleValidationErrors option', async () => { - const testInstance = createTestkit([useMaskedErrors({ handleValidationErrors: true })], schema); - const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); - }); - it('should use custom error formatter for execution errors', async () => { - const customErrorFormatter: FormatErrorHandler = e => + it('should use custom error mask function for execution errors', async () => { + const customErrorMaskFn: MaskError = e => new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { custom: true, }); - const testInstance = createTestkit([useMaskedErrors({ formatError: customErrorFormatter })], schema); - const result = await testInstance.execute(`query { secret }`); + const testInstance = createTestkit([useMaskedErrors({ maskError: customErrorMaskFn })], schema); + const result = await testInstance.perform({ query: `query { secret }` }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { @@ -448,16 +390,19 @@ describe('useMaskedErrors', () => { ], } `); + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"errors\\":[{\\"message\\":\\"Custom error message for Secret sauce that should not leak.\\\\n\\\\nGraphQL request:1:9\\\\n1 | query { secret }\\\\n | ^\\",\\"extensions\\":{\\"custom\\":true}}],\\"data\\":null}"` + ); }); - it('should use custom error formatter for subscribe (AsyncIterable) subscription errors', async () => { - const customErrorFormatter: FormatErrorHandler = e => + it('should use custom error mask function for subscribe (AsyncIterable) subscription errors', async () => { + const customErrorMaskFn: MaskError = e => new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { custom: true, }); expect.assertions(2); - const testInstance = createTestkit([useMaskedErrors({ formatError: customErrorFormatter })], schema); - const resultStream = await testInstance.execute(`subscription { streamError }`); + const testInstance = createTestkit([useMaskedErrors({ maskError: customErrorMaskFn })], schema); + const resultStream = await testInstance.perform({ query: `subscription { streamError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -467,72 +412,74 @@ describe('useMaskedErrors', () => { } }); - it('should use custom error formatter for parsing errors with handleParseErrors options', async () => { - const customErrorFormatter: FormatErrorHandler = e => - new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { - custom: true, - }); - const useMyFailingParser: Plugin = { - onParse(payload) { - payload.setParseFn(() => { - throw new GraphQLError('My custom error'); - }); - }, - }; - const testInstance = createTestkit( - [useMaskedErrors({ formatError: customErrorFormatter, handleParseErrors: true }), useMyFailingParser], - schema - ); - const result = await testInstance.execute(`query { a `, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual('Custom error message for My custom error'); - expect(error.extensions).toEqual({ custom: true }); - }); - it('should use custom error formatter for validation errors with handleValidationErrors option', async () => { - const customErrorFormatter: FormatErrorHandler = e => - new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { - custom: true, - }); - const useMyFailingValidator: Plugin = { - onValidate(payload) { - payload.setValidationFn(() => { - return [new GraphQLError('My custom error')]; - }); - }, - }; - const testInstance = createTestkit( - [useMaskedErrors({ formatError: customErrorFormatter, handleValidationErrors: true }), useMyFailingValidator], - schema - ); - const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual('Custom error message for My custom error'); - expect(error.extensions).toEqual({ custom: true }); - }); - it('should use custom error formatter for errors while building the context', async () => { - const customErrorFormatter: FormatErrorHandler = e => + it('should use custom error mask function for errors while building the context', async () => { + const customErrorMaskFn: MaskError = e => new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { custom: true, }); const testInstance = createTestkit( [ - useMaskedErrors({ formatError: customErrorFormatter }), + useMaskedErrors({ maskError: customErrorMaskFn }), useExtendContext(() => { - throw new GraphQLError('Custom error'); + throw createGraphQLError('Custom error'); return {}; }), ], schema ); try { - await testInstance.execute(`query { secret }`, {}, {}); + await testInstance.perform({ query: `query { secret }` }); } catch (e) { expect((e as GraphQLError).message).toEqual('Custom error message for Custom error'); } expect.assertions(1); }); + + it('should include the original error message stack in the extensions in development mode', async () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: String + } + `, + resolvers: { + Query: { + foo: () => { + throw new Error("I'm a teapot"); + }, + }, + }, + }); + const testInstance = createTestkit([useMaskedErrors({ maskError: createDefaultMaskError(true) })], schema); + const result = await testInstance.perform({ query: `query { foo }` }); + assertSingleExecutionValue(result); + expect(result.errors?.[0].extensions).toEqual({ + message: "I'm a teapot", + stack: expect.stringMatching(/^Error: I'm a teapot/), + }); + }); + + it('should include the original thrown thing in the extensions in development mode', async () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: String + } + `, + resolvers: { + Query: { + foo: () => { + throw "I'm a teapot"; + }, + }, + }, + }); + const testInstance = createTestkit([useMaskedErrors({ maskError: createDefaultMaskError(true) })], schema); + const result = await testInstance.perform({ query: `query { foo }` }); + assertSingleExecutionValue(result); + expect(result.errors?.[0].extensions).toEqual({ + message: 'Unexpected error value: "I\'m a teapot"', + stack: expect.stringMatching(/NonErrorThrown: Unexpected error value: \"I'm a teapot/), + }); + }); }); diff --git a/packages/core/test/subscribe.spec.ts b/packages/core/test/subscribe.spec.ts index 9c1709aa60..ad4865b65d 100644 --- a/packages/core/test/subscribe.spec.ts +++ b/packages/core/test/subscribe.spec.ts @@ -31,11 +31,13 @@ describe('subscribe', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - subscription { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + subscription { + alphabet + } + `, + }); assertStreamExecutionValue(result); const values = await collectAsyncIteratorValues(result); expect(values).toEqual([ @@ -79,11 +81,13 @@ describe('subscribe', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - subscription { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + subscription { + alphabet + } + `, + }); assertStreamExecutionValue(result); await collectAsyncIteratorValues(result); }); diff --git a/packages/core/test/utils.spec.ts b/packages/core/test/utils.spec.ts deleted file mode 100644 index 01a393f340..0000000000 --- a/packages/core/test/utils.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useLogger, enableIf } from '@envelop/core'; -import { createTestkit, createSpiedPlugin } from '@envelop/testing'; -import { getIntrospectionQuery, parse } from 'graphql'; -import { isIntrospectionDocument } from '../src/utils.js'; -import { query, schema } from './common.js'; - -describe('Utils', () => { - describe('isIntrospectionDocument', () => { - it('Should return false on non-introspection', () => { - const doc = `query test { f }`; - - expect(isIntrospectionDocument(parse(doc))).toBeFalsy(); - }); - const introspectionFields = ['__schema', '__type']; - introspectionFields.forEach(introspectionFieldName => { - it(`Should detect ${introspectionFieldName} original introspection query`, () => { - const doc = getIntrospectionQuery(); - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - - it('Should detect minimal introspection', () => { - const doc = `query { ${introspectionFieldName} { test }}`; - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - - it('Should detect alias tricks', () => { - const doc = `query { test: ${introspectionFieldName} { test }}`; - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - - it('Should detect inline fragment tricks', () => { - const doc = `query { ... on Query { ${introspectionFieldName} { test } } }`; - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - - it('should detect fragment spread tricks', () => { - const doc = `fragment Fragment on Query { ${introspectionFieldName} } query { ...Fragment }`; - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - }); - }); - - describe('enableIf', () => { - it('Should return a plugin', () => { - const plugin = enableIf(true, useLogger()); - expect(plugin).toBeTruthy(); - }); - - it('Should return null', () => { - const plugin = enableIf(false, useLogger()); - expect(plugin).toBeFalsy(); - }); - - it('Should not init plugin', async () => { - const spiedPlugin = createSpiedPlugin(); - const testkit = createTestkit([enableIf(false, spiedPlugin.plugin)], schema); - await testkit.execute(query); - expect(spiedPlugin.spies.beforeExecute).not.toHaveBeenCalled(); - }); - - it('Should init plugin', async () => { - const spiedPlugin = createSpiedPlugin(); - const testkit = createTestkit([enableIf(true, spiedPlugin.plugin)], schema); - await testkit.execute(query); - expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/core/test/validate.spec.ts b/packages/core/test/validate.spec.ts index 6931df84d6..89b895332b 100644 --- a/packages/core/test/validate.spec.ts +++ b/packages/core/test/validate.spec.ts @@ -6,7 +6,7 @@ describe('validate', () => { it('Should call before validate and after validate correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query); + await teskit.perform({ query }); expect(spiedPlugin.spies.beforeValidate).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeValidate).toHaveBeenCalledWith({ context: expect.any(Object), @@ -46,7 +46,7 @@ describe('validate', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(1); expect(replacementFn).toHaveBeenCalledWith( expect.any(GraphQLSchema), @@ -70,7 +70,7 @@ describe('validate', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(0); }); @@ -92,7 +92,7 @@ describe('validate', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(after).toHaveBeenCalledTimes(1); expect(after).toHaveBeenCalledWith({ valid: false, @@ -108,7 +108,7 @@ describe('validate', () => { [ { onValidate: ({ addValidationRule }) => { - addValidationRule(context => { + addValidationRule((context: any) => { context.reportError(new GraphQLError('Invalid!')); return {}; }); @@ -118,7 +118,7 @@ describe('validate', () => { schema ); - const r = await teskit.execute(query); + const r = await teskit.perform({ query }); assertSingleExecutionValue(r); expect(r.errors).toBeDefined(); @@ -140,7 +140,7 @@ describe('validate', () => { schema ); - const r = await teskit.execute(query); + const r = await teskit.perform({ query }); assertSingleExecutionValue(r); expect(r.errors).toBeDefined(); diff --git a/packages/plugins/apollo-datasources/README.md b/packages/plugins/apollo-datasources/README.md index 76ffc8135d..2debde0172 100644 --- a/packages/plugins/apollo-datasources/README.md +++ b/packages/plugins/apollo-datasources/README.md @@ -11,6 +11,7 @@ yarn add @envelop/apollo-datasources ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloDataSources } from '@envelop/apollo-datasources' import { RESTDataSource } from 'apollo-datasource-rest' @@ -35,6 +36,10 @@ class MoviesAPI extends RESTDataSource { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloDataSources({ diff --git a/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts b/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts index 609a85b60c..fab9a8bf25 100644 --- a/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts +++ b/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts @@ -31,9 +31,8 @@ describe('useApolloDataSources', () => { ], schema ); - const result = await testInstance.execute( - `query { foo }`, - {}, + const result = await testInstance.perform( + { query: `query { foo }` }, { initialContextValue: true, } @@ -80,7 +79,7 @@ describe('useApolloDataSources', () => { ], schema ); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); diff --git a/packages/plugins/apollo-federation/README.md b/packages/plugins/apollo-federation/README.md index a5efe100bb..45a015c36b 100644 --- a/packages/plugins/apollo-federation/README.md +++ b/packages/plugins/apollo-federation/README.md @@ -12,6 +12,7 @@ yarn add @envelop/apollo-federation ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { ApolloGateway } from '@apollo/gateway' import { useApolloFederation } from '@envelop/apollo-federation' @@ -29,6 +30,10 @@ await gateway.load() // Then pass it to the plugin configuration const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloFederation({ gateway }) diff --git a/packages/plugins/apollo-federation/test/federation.spec.ts b/packages/plugins/apollo-federation/test/federation.spec.ts index 823ea5bbc6..b1f8aabcec 100644 --- a/packages/plugins/apollo-federation/test/federation.spec.ts +++ b/packages/plugins/apollo-federation/test/federation.spec.ts @@ -62,7 +62,7 @@ describe('useApolloFederation', () => { }, ]); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(onExecuteSpy).toHaveBeenCalledTimes(1); expect(onExecuteSpy.mock.calls[0][0].executeFn).not.toBe(execute); @@ -71,7 +71,7 @@ describe('useApolloFederation', () => { it('Should execute document string correctly', async () => { const testInstance = createTestkit([useTestFederation()]); - const result = await testInstance.execute(query); + const result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeFalsy(); expect(result.data).toMatchInlineSnapshot(` @@ -101,7 +101,7 @@ Object { it('Should execute parsed document correctly', async () => { const testInstance = createTestkit([useTestFederation()]); - const result = await testInstance.execute(parse(query)); + const result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeFalsy(); expect(result.data).toMatchInlineSnapshot(` diff --git a/packages/plugins/apollo-server-errors/README.md b/packages/plugins/apollo-server-errors/README.md index 51625fd3e4..e82b24f7a0 100644 --- a/packages/plugins/apollo-server-errors/README.md +++ b/packages/plugins/apollo-server-errors/README.md @@ -11,10 +11,15 @@ yarn add @envelop/apollo-server-errors ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloServerErrors } from '@envelop/apollo-server-errors' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloServerErrors({ diff --git a/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts b/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts index 5e3c02b6e4..c7a2c36afc 100644 --- a/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts +++ b/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts @@ -4,6 +4,7 @@ import { GraphQLSchema } from 'graphql'; import { envelop, useSchema } from '@envelop/core'; import { useApolloServerErrors } from '../src/index.js'; import { assertSingleExecutionValue } from '@envelop/testing'; +import { parse, execute, validate, subscribe } from 'graphql'; // Fix compat by mocking broken function // we can remove this once apollo fixed legacy usages of execute(schema, ...args) @@ -15,7 +16,13 @@ jest.mock('../../../../node_modules/apollo-server-core/dist/utils/schemaHash', ( describe('useApolloServerErrors', () => { const executeBoth = async (schema: GraphQLSchema, query: string, debug: boolean) => { const apolloServer = new ApolloServerBase({ schema, debug }); - const envelopRuntime = envelop({ plugins: [useSchema(schema), useApolloServerErrors({ debug })] })({}); + const envelopRuntime = envelop({ + plugins: [useSchema(schema), useApolloServerErrors({ debug })], + parse, + execute, + validate, + subscribe, + })({}); return { apollo: await apolloServer.executeOperation({ query }), diff --git a/packages/plugins/apollo-tracing/README.md b/packages/plugins/apollo-tracing/README.md index eec2b253ef..969af079f0 100644 --- a/packages/plugins/apollo-tracing/README.md +++ b/packages/plugins/apollo-tracing/README.md @@ -17,10 +17,15 @@ yarn add @envelop/apollo-tracing ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloTracing } from '@envelop/apollo-tracing' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloTracing() diff --git a/packages/plugins/apollo-tracing/package.json b/packages/plugins/apollo-tracing/package.json index a0409ca46b..c844e92801 100644 --- a/packages/plugins/apollo-tracing/package.json +++ b/packages/plugins/apollo-tracing/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "apollo-tracing": "^0.15.0", "tslib": "^2.4.0" }, @@ -56,6 +57,7 @@ "typescript": "4.7.4" }, "peerDependencies": { + "@envelop/types": "^2.4.0", "@envelop/core": "^2.6.0", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, diff --git a/packages/plugins/apollo-tracing/src/index.ts b/packages/plugins/apollo-tracing/src/index.ts index f6d7a3c0a9..fd6fcb74b8 100644 --- a/packages/plugins/apollo-tracing/src/index.ts +++ b/packages/plugins/apollo-tracing/src/index.ts @@ -1,4 +1,5 @@ import { Plugin, handleStreamOrSingleExecutionResult } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { TracingFormat } from 'apollo-tracing'; import { GraphQLType, ResponsePath, responsePathAsArray } from 'graphql'; @@ -41,21 +42,24 @@ type TracingContextObject = { export const useApolloTracing = (): Plugin => { return { - onResolverCalled: ({ info, context }) => { - const ctx = context[apolloTracingSymbol] as TracingContextObject; - // Taken from https://github.com/apollographql/apollo-server/blob/main/packages/apollo-tracing/src/index.ts - const resolverCall: ResolverCall = { - path: info.path, - fieldName: info.fieldName, - parentType: info.parentType, - returnType: info.returnType, - startOffset: process.hrtime(ctx.hrtime), - }; - - return () => { - resolverCall.endOffset = process.hrtime(ctx.hrtime); - ctx.resolversTiming.push(resolverCall); - }; + onPluginInit({ addPlugin }) { + addPlugin( + useOnResolve(({ info, context }) => { + const ctx = context[apolloTracingSymbol] as TracingContextObject; + // Taken from https://github.com/apollographql/apollo-server/blob/main/packages/apollo-tracing/src/index.ts + const resolverCall: ResolverCall = { + path: info.path, + fieldName: info.fieldName, + parentType: info.parentType, + returnType: info.returnType, + startOffset: process.hrtime(ctx.hrtime), + }; + return () => { + resolverCall.endOffset = process.hrtime(ctx.hrtime); + ctx.resolversTiming.push(resolverCall); + }; + }) + ); }, onExecute(onExecuteContext) { const ctx: TracingContextObject = { diff --git a/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts b/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts index 2f0ca667b5..8502982e0a 100644 --- a/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts +++ b/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts @@ -15,7 +15,7 @@ describe('useApolloTracing', () => { it('should measure execution times and return it as extension', async () => { const testInstance = createTestkit([useApolloTracing()], schema); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); diff --git a/packages/plugins/auth0/README.md b/packages/plugins/auth0/README.md index de98028bb9..c87789787a 100644 --- a/packages/plugins/auth0/README.md +++ b/packages/plugins/auth0/README.md @@ -14,10 +14,15 @@ We recommend using the [Adding Authentication with Auth0 guide](https://www.enve 4. Setup Envelop with that plugin: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useAuth0 } from '@envelop/auth0' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useAuth0({ diff --git a/packages/plugins/auth0/package.json b/packages/plugins/auth0/package.json index 2e48a3d3d3..71a73cce0f 100644 --- a/packages/plugins/auth0/package.json +++ b/packages/plugins/auth0/package.json @@ -53,12 +53,10 @@ }, "devDependencies": { "@types/jsonwebtoken": "8.5.8", - "graphql": "16.3.0", "typescript": "4.7.4" }, "peerDependencies": { - "@envelop/core": "^2.6.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "@envelop/core": "^2.6.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/plugins/auth0/src/index.ts b/packages/plugins/auth0/src/index.ts index 7c22ec4487..d7a1a6e7fc 100644 --- a/packages/plugins/auth0/src/index.ts +++ b/packages/plugins/auth0/src/index.ts @@ -1,9 +1,8 @@ /* eslint-disable no-console */ /* eslint-disable dot-notation */ -import { EnvelopError, Plugin } from '@envelop/core'; +import { Plugin } from '@envelop/core'; import * as JwksRsa from 'jwks-rsa'; import jwtPkg, { VerifyOptions, DecodeOptions } from 'jsonwebtoken'; -import { GraphQLError } from 'graphql'; const { decode, verify } = jwtPkg; @@ -23,7 +22,7 @@ export type Auth0PluginOptions = { headerName?: string; }; -export class UnauthenticatedError extends GraphQLError {} +export class UnauthenticatedError extends Error {} export type UserPayload = { sub: string; @@ -71,12 +70,12 @@ export const useAuth0 = (options: TOptions) const split = authHeader.split(' '); if (split.length !== 2) { - throw new EnvelopError(`Invalid value provided for header "${headerName}"!`); + throw new Error(`Invalid value provided for header "${headerName}"!`); } else { const [type, value] = split; if (type !== tokenType) { - throw new EnvelopError(`Unsupported token type provided: "${type}"!`); + throw new Error(`Unsupported token type provided: "${type}"!`); } else { return value; } diff --git a/packages/plugins/dataloader/README.md b/packages/plugins/dataloader/README.md index 5658f9aed5..5c41022461 100644 --- a/packages/plugins/dataloader/README.md +++ b/packages/plugins/dataloader/README.md @@ -11,11 +11,16 @@ yarn add dataloader @envelop/dataloader ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import DataLoader from 'dataloader' import { useDataLoader } from '@envelop/dataloader' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useDataLoader('users', context => new DataLoader(keys => myBatchGetUsers(keys))) diff --git a/packages/plugins/dataloader/package.json b/packages/plugins/dataloader/package.json index 60d6f614c8..b598ee86c7 100644 --- a/packages/plugins/dataloader/package.json +++ b/packages/plugins/dataloader/package.json @@ -56,9 +56,8 @@ "typescript": "4.7.4" }, "peerDependencies": { - "@envelop/core": "^2.6.0", "dataloader": "^2.0.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "@envelop/core": "^2.6.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/plugins/dataloader/test/dataloader.spec.ts b/packages/plugins/dataloader/test/dataloader.spec.ts index 297a79bb72..e8c84c601b 100644 --- a/packages/plugins/dataloader/test/dataloader.spec.ts +++ b/packages/plugins/dataloader/test/dataloader.spec.ts @@ -27,7 +27,7 @@ describe('useDataLoader', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.data?.test).toBe('myValue'); }); diff --git a/packages/plugins/depth-limit/README.md b/packages/plugins/depth-limit/README.md index 90efe640eb..589ac67e4b 100644 --- a/packages/plugins/depth-limit/README.md +++ b/packages/plugins/depth-limit/README.md @@ -11,10 +11,15 @@ yarn add @envelop/depth-limit ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useDepthLimit } from '@envelop/depth-limit' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useDepthLimit({ diff --git a/packages/plugins/disable-introspection/README.md b/packages/plugins/disable-introspection/README.md index 0e16f4e4fc..9cbe9c5b71 100644 --- a/packages/plugins/disable-introspection/README.md +++ b/packages/plugins/disable-introspection/README.md @@ -11,10 +11,15 @@ yarn add @envelop/disable-introspection ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useDisableIntrospection } from '@envelop/disable-introspection' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useDisableIntrospection()] }) ``` diff --git a/packages/plugins/execute-subscription-event/README.md b/packages/plugins/execute-subscription-event/README.md index b4849c3e41..715f06d42a 100644 --- a/packages/plugins/execute-subscription-event/README.md +++ b/packages/plugins/execute-subscription-event/README.md @@ -7,11 +7,16 @@ Utilities for hooking into the [ExecuteSubscriptionEvent]( createContext()), useContextValuePerExecuteSubscriptionEvent(() => ({ diff --git a/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts b/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts index f921d2113a..7aa94118b3 100644 --- a/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts +++ b/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts @@ -27,7 +27,7 @@ describe('useContextValuePerExecuteSubscriptionEvent', () => { schema ); - const result = await testInstance.execute(subscriptionOperationString); + const result = await testInstance.perform({ query: subscriptionOperationString }); assertStreamExecutionValue(result); pushValue({}); @@ -63,7 +63,7 @@ describe('useContextValuePerExecuteSubscriptionEvent', () => { schema ); - const result = await testInstance.execute(subscriptionOperationString); + const result = await testInstance.perform({ query: subscriptionOperationString }); assertStreamExecutionValue(result); pushValue({}); diff --git a/packages/plugins/extended-validation/README.md b/packages/plugins/extended-validation/README.md index d1b90ca308..fbaded9aba 100644 --- a/packages/plugins/extended-validation/README.md +++ b/packages/plugins/extended-validation/README.md @@ -17,9 +17,15 @@ yarn add @envelop/extended-validation Then, use the plugin with your validation rules: ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' import { useExtendedValidation } from '@envelop/extended-validation' -const getEnveloped = evelop({ +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useExtendedValidation({ rules: [ diff --git a/packages/plugins/extended-validation/src/plugin.ts b/packages/plugins/extended-validation/src/plugin.ts index a21b6efaaf..1dfecb7721 100644 --- a/packages/plugins/extended-validation/src/plugin.ts +++ b/packages/plugins/extended-validation/src/plugin.ts @@ -24,13 +24,13 @@ type OnValidationFailedCallback = (params: { setResult: (result: ExecutionResult) => void; }) => void; -export const useExtendedValidation = (options: { +export const useExtendedValidation = = {}>(options: { rules: Array; /** * Callback that is invoked if the extended validation yields any errors. */ onValidationFailed?: OnValidationFailedCallback; -}): Plugin => { +}): Plugin => { let schemaTypeInfo: TypeInfo; function getTypeInfo(): TypeInfo | undefined { @@ -50,6 +50,7 @@ export const useExtendedValidation = (options: { didRun: false, }; extendContext({ + ...context, [symbolExtendedValidationRules]: validationRulesContext, }); } diff --git a/packages/plugins/extended-validation/test/extended-validation.spec.ts b/packages/plugins/extended-validation/test/extended-validation.spec.ts index e99f740395..f9d45ca3b8 100644 --- a/packages/plugins/extended-validation/test/extended-validation.spec.ts +++ b/packages/plugins/extended-validation/test/extended-validation.spec.ts @@ -3,6 +3,7 @@ import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { buildSchema, GraphQLError, parse } from 'graphql'; import { useExtendedValidation } from '../src/index.js'; +import { parse as gqlParse, execute as gqlExecute, validate as gqlValidate, subscribe as gqlSubscribe } from 'graphql'; describe('useExtendedValidation', () => { it('supports usage of multiple useExtendedValidation in different plugins', async () => { @@ -44,7 +45,7 @@ describe('useExtendedValidation', () => { schema ); - const result = await testInstance.execute(operation); + const result = await testInstance.perform({ query: operation }); expect(result).toMatchInlineSnapshot(` Object { "data": null, @@ -93,7 +94,7 @@ describe('useExtendedValidation', () => { schema ); - await testInstance.execute(operation); + await testInstance.perform({ query: operation }); expect(extendedValidationRunCount).toEqual(1); }); it('execute throws an error if "contextFactory" has not been invoked', async () => { @@ -114,6 +115,10 @@ describe('useExtendedValidation', () => { rules: [() => ({})], }), ], + parse: gqlParse, + execute: gqlExecute, + validate: gqlValidate, + subscribe: gqlSubscribe, })(); await expect( execute({ @@ -164,7 +169,7 @@ describe('useExtendedValidation', () => { ], schema ); - const result = await testkit.execute(operation); + const result = await testkit.perform({ query: operation }); expect(calledExtendedValidationRule).toEqual(true); }); it('subscribe does result in extended validation phase errors', async () => { @@ -205,7 +210,7 @@ describe('useExtendedValidation', () => { ], schema ); - const result = await testkit.execute(operation); + const result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { diff --git a/packages/plugins/extended-validation/test/one-of.spec.ts b/packages/plugins/extended-validation/test/one-of.spec.ts index 4d55b435ca..1ba0a24f02 100644 --- a/packages/plugins/extended-validation/test/one-of.spec.ts +++ b/packages/plugins/extended-validation/test/one-of.spec.ts @@ -529,7 +529,7 @@ describe('oneOf', () => { testSchema ); - const result = await testInstance.execute(document, variables); + const result = await testInstance.perform({ query: document, variables }); assertSingleExecutionValue(result); if (expectedError) { expect(result.errors).toBeDefined(); @@ -631,7 +631,7 @@ describe('oneOf', () => { testSchema ); - const result = await testInstance.execute(document, variables); + const result = await testInstance.perform({ query: document, variables }); assertSingleExecutionValue(result); if (expectedError) { expect(result.errors).toBeDefined(); diff --git a/packages/plugins/filter-operation-type/README.md b/packages/plugins/filter-operation-type/README.md index 86b080ac0c..eb02bddb1f 100644 --- a/packages/plugins/filter-operation-type/README.md +++ b/packages/plugins/filter-operation-type/README.md @@ -11,9 +11,15 @@ yarn add @envelop/filter-operation-type ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useFilterAllowedOperations } from '@envelop/filter-operation-type' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, // only allow execution of subscription operations plugins: [useFilterAllowedOperations(['subscription'])] }) diff --git a/packages/plugins/fragment-arguments/README.md b/packages/plugins/fragment-arguments/README.md index fac953a4b1..ed8ce1e90d 100644 --- a/packages/plugins/fragment-arguments/README.md +++ b/packages/plugins/fragment-arguments/README.md @@ -15,10 +15,15 @@ yarn add @envelop/fragment-arguments ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useFragmentArguments } from '@envelop/fragment-arguments' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useFragmentArguments() diff --git a/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts b/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts index ab76dece19..4149932d95 100644 --- a/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts +++ b/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts @@ -3,6 +3,7 @@ import { oneLine, stripIndent } from 'common-tags'; import { diff } from 'jest-diff'; import { envelop, useSchema } from '@envelop/core'; import { useFragmentArguments } from '../src/index.js'; +import { parse as gqlParse, execute as gqlExecute, validate as gqlValidate, subscribe as gqlSubscribe } from 'graphql'; function compareStrings(a: string, b: string): boolean { return a.includes(b); @@ -66,7 +67,13 @@ describe('useFragmentArguments', () => { } `); test('can inline fragment with argument', () => { - const { parse } = envelop({ plugins: [useFragmentArguments(), useSchema(schema)] })({}); + const { parse } = envelop({ + plugins: [useFragmentArguments(), useSchema(schema)], + parse: gqlParse, + execute: gqlExecute, + validate: gqlValidate, + subscribe: gqlSubscribe, + })({}); const result = parse(/* GraphQL */ ` fragment TestFragment($c: String) on Query { a(b: $c) diff --git a/packages/plugins/generic-auth/README.md b/packages/plugins/generic-auth/README.md index 3908f0e76b..ba31b63414 100644 --- a/packages/plugins/generic-auth/README.md +++ b/packages/plugins/generic-auth/README.md @@ -78,6 +78,7 @@ This mode offers complete protection for the entire API. It protects your entire To setup this mode, use the following config: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useGenericAuth, ResolveUserFn, ValidateUserFn } from '@envelop/generic-auth' @@ -92,6 +93,10 @@ const validateUser: ValidateUserFn = params => { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useGenericAuth({ diff --git a/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts b/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts index 3c0b706c0a..397d62256f 100644 --- a/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts +++ b/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts @@ -70,7 +70,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(getIntrospectionQuery()); + const result = await testInstance.perform({ query: getIntrospectionQuery() }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -86,7 +86,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.test).toBe('Dotan'); @@ -104,7 +104,7 @@ describe('useGenericAuth', () => { ); try { - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); } catch (err) { expect(err).toMatchInlineSnapshot(`[GraphQLError: Unauthenticated!]`); } @@ -125,7 +125,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(spyFn).toHaveBeenCalledWith( @@ -153,7 +153,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -170,7 +170,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -189,7 +189,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.test).toBe('Dotan'); @@ -206,7 +206,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.test).toBe(''); @@ -227,7 +227,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(spyFn).toHaveBeenCalledWith( @@ -259,7 +259,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(spyFn).toHaveBeenCalledWith( @@ -304,7 +304,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { protected }`); + const result = await testInstance.perform({ query: `query { protected }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.protected).toBe('Dotan'); @@ -321,7 +321,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -338,7 +338,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -355,7 +355,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { protected }`); + const result = await testInstance.perform({ query: `query { protected }` }); assertSingleExecutionValue(result); expect(result.errors?.length).toBe(1); expect(result.errors?.[0].message).toBe(`Accessing 'Query.protected' requires authentication.`); @@ -442,7 +442,7 @@ describe('useGenericAuth', () => { schemaWithDirectiveWithRole ); - const result = await testInstance.execute(`query { admin }`); + const result = await testInstance.perform({ query: `query { admin }` }); assertSingleExecutionValue(result); expect(result.errors?.length).toBe(1); expect(result.errors?.[0].message).toBe( @@ -462,7 +462,7 @@ describe('useGenericAuth', () => { schemaWithDirectiveWithRole ); - const result = await testInstance.execute(`query { admin }`); + const result = await testInstance.perform({ query: `query { admin }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.admin).toBe('admin'); diff --git a/packages/plugins/graphql-jit/README.md b/packages/plugins/graphql-jit/README.md index 9997b13bd5..45ec30327b 100644 --- a/packages/plugins/graphql-jit/README.md +++ b/packages/plugins/graphql-jit/README.md @@ -11,10 +11,15 @@ yarn add @envelop/graphql-jit ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useGraphQlJit } from '@envelop/graphql-jit' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useGraphQlJit( diff --git a/packages/plugins/graphql-jit/test/graphql-jit.spec.ts b/packages/plugins/graphql-jit/test/graphql-jit.spec.ts index 1fdc81f9c8..c357835cb6 100644 --- a/packages/plugins/graphql-jit/test/graphql-jit.spec.ts +++ b/packages/plugins/graphql-jit/test/graphql-jit.spec.ts @@ -48,7 +48,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); expect(onExecuteSpy).toHaveBeenCalledTimes(1); expect(onExecuteSpy.mock.calls[0][0].executeFn).not.toBe(execute); @@ -67,7 +67,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`subscription { count }`); + await testInstance.perform({ query: `subscription { count }` }); expect(onSubscribeSpy).toHaveBeenCalledTimes(1); expect(onSubscribeSpy.mock.calls[0][0].subscribeFn).not.toBe(subscribe); @@ -91,7 +91,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); expect(onExecuteSpy).toHaveBeenCalledTimes(1); expect(onExecuteSpy.mock.calls[0][0].executeFn).toBe(execute); @@ -116,7 +116,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`subscription { count }`); + await testInstance.perform({ query: `subscription { count }` }); expect(onSubscribeSpy).toHaveBeenCalledTimes(1); expect(onSubscribeSpy.mock.calls[0][0].subscribeFn).toBe(subscribe); @@ -125,14 +125,14 @@ describe('useGraphQlJit', () => { it('Should execute correctly', async () => { const testInstance = createTestkit([useGraphQlJit()], schema); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.data?.test).toBe('boop'); }); it('Should subscribe correctly', async () => { const testInstance = createTestkit([useGraphQlJit()], schema); - const result = await testInstance.execute(`subscription { count }`); + const result = await testInstance.perform({ query: `subscription { count }` }); assertStreamExecutionValue(result); const values = await collectAsyncIteratorValues(result); for (let i = 0; i < 10; i++) { @@ -157,7 +157,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); expect(cache.get).toHaveBeenCalled(); expect(cache.set).toHaveBeenCalled(); }); diff --git a/packages/plugins/graphql-middleware/README.md b/packages/plugins/graphql-middleware/README.md index 6b285e41f9..85c7503702 100644 --- a/packages/plugins/graphql-middleware/README.md +++ b/packages/plugins/graphql-middleware/README.md @@ -15,6 +15,7 @@ yarn add graphql-middleware @envelop/graphql-middleware You can use any type of middleware defined for `graphql-middleware`, here's an example for doing that with [`graphql-shield`](https://github.com/maticzav/graphql-shield): ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useGraphQLMiddleware } from '@envelop/graphql-middleware' import { rule, shield, and, or, not } from 'graphql-shield' @@ -35,6 +36,10 @@ const permissions = shield({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useSchema(mySchema), diff --git a/packages/plugins/graphql-middleware/src/index.ts b/packages/plugins/graphql-middleware/src/index.ts index ece38586bf..a2cc564a2f 100644 --- a/packages/plugins/graphql-middleware/src/index.ts +++ b/packages/plugins/graphql-middleware/src/index.ts @@ -8,7 +8,6 @@ export const useGraphQLMiddleware = ): Plugin => { return { onSchemaChange({ schema, replaceSchema }) { - // @ts-expect-error See https://github.com/graphql/graphql-js/pull/3511 - remove this comments once merged if (schema.extensions?.[graphqlMiddlewareAppliedTransformSymbol]) { return; } diff --git a/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts b/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts index 5f4253d258..6e89062c80 100644 --- a/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts +++ b/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts @@ -28,6 +28,6 @@ describe('useGraphQlJit', () => { schema ); - await testkit.execute(`{ __typename}`); + await testkit.perform({ query: '{ __typename}' }); }); }); diff --git a/packages/plugins/graphql-modules/README.md b/packages/plugins/graphql-modules/README.md index 9b1218cdd2..fe3c256a2a 100644 --- a/packages/plugins/graphql-modules/README.md +++ b/packages/plugins/graphql-modules/README.md @@ -13,6 +13,7 @@ yarn add @envelop/graphql-modules ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { createApplication } from 'graphql-modules' import { useGraphQLModules } from '@envelop/graphql-modules' @@ -24,6 +25,10 @@ const myApp = createApplication({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useGraphQLModules(myApp) diff --git a/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts b/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts index 2302f06406..777ebd6097 100644 --- a/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts +++ b/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts @@ -38,7 +38,7 @@ describe('useGraphQLModules', () => { it('Should work correctly and init all providers at the right time', async () => { const testInstance = createTestkit([useGraphQLModules(app)]); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('testFoo'); }); diff --git a/packages/plugins/immediate-introspection/.npmignore b/packages/plugins/immediate-introspection/.npmignore new file mode 100644 index 0000000000..3684decc03 --- /dev/null +++ b/packages/plugins/immediate-introspection/.npmignore @@ -0,0 +1,2 @@ +test +*.png diff --git a/packages/core/docs/use-immediate-introspection.md b/packages/plugins/immediate-introspection/README.md similarity index 82% rename from packages/core/docs/use-immediate-introspection.md rename to packages/plugins/immediate-introspection/README.md index e22252102f..5f3d0f1d07 100644 --- a/packages/core/docs/use-immediate-introspection.md +++ b/packages/plugins/immediate-introspection/README.md @@ -1,4 +1,12 @@ -#### `useImmediateIntrospection` +## `@envelop/immediate-introspection` + +## Getting Started + +``` +yarn add @envelop/immediate-introspection +``` + +## Usage Example Context building can be costly and require calling remote services. For simple GraphQL operations that only select introspection fields building a context is not necessary. @@ -6,10 +14,15 @@ For simple GraphQL operations that only select introspection fields building a c The `useImmediateIntrospection` can be used to short circuit any further context building if a GraphQL operation selection set only includes introspection fields within the selection set. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop, useImmediateIntrospection } from '@envelop/core' import { schema } from './schema' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useImmediateIntrospection() diff --git a/packages/plugins/immediate-introspection/package.json b/packages/plugins/immediate-introspection/package.json new file mode 100644 index 0000000000..5feedb43ce --- /dev/null +++ b/packages/plugins/immediate-introspection/package.json @@ -0,0 +1,67 @@ +{ + "name": "@envelop/immediate-introspection", + "version": "0.0.0", + "author": "Saihajpreet Singh ", + "license": "MIT", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/n1ru4l/envelop.git", + "directory": "packages/plugins/immediate-introspection" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./*": { + "require": { + "types": "./dist/typings/*.d.cts", + "default": "./dist/cjs/*.js" + }, + "import": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + }, + "default": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "dependencies": {}, + "devDependencies": { + "graphql": "16.3.0", + "typescript": "4.7.4" + }, + "peerDependencies": { + "@envelop/core": "^2.5.0", + "@sentry/node": "^6 || ^7", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "type": "module" +} diff --git a/packages/core/src/plugins/use-immediate-introspection.ts b/packages/plugins/immediate-introspection/src/index.ts similarity index 100% rename from packages/core/src/plugins/use-immediate-introspection.ts rename to packages/plugins/immediate-introspection/src/index.ts diff --git a/packages/core/test/plugins/use-immediate-introspection.spec.ts b/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts similarity index 70% rename from packages/core/test/plugins/use-immediate-introspection.spec.ts rename to packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts index 9c563768c4..72318c9400 100644 --- a/packages/core/test/plugins/use-immediate-introspection.spec.ts +++ b/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts @@ -1,7 +1,7 @@ import { createTestkit } from '@envelop/testing'; -import { useImmediateIntrospection } from '../../src/plugins/use-immediate-introspection.js'; -import { useExtendContext } from '../../src/plugins/use-extend-context.js'; -import { schema } from '../common.js'; +import { useImmediateIntrospection } from '../src/index.js'; +import { useExtendContext } from '@envelop/core'; +import { schema } from '../../../core/test/common.js'; import { getIntrospectionQuery } from 'graphql'; describe('useImmediateIntrospection', () => { @@ -11,11 +11,13 @@ describe('useImmediateIntrospection', () => { schema ); - await testInstance.execute(/* GraphQL */ ` - query { - __typename - } - `); + await testInstance.perform({ + query: /* GraphQL */ ` + query { + __typename + } + `, + }); }); it('skips context building for introspection only operation (alias)', async () => { const testInstance = createTestkit( @@ -23,11 +25,13 @@ describe('useImmediateIntrospection', () => { schema ); - await testInstance.execute(/* GraphQL */ ` - query { - some: __typename - } - `); + await testInstance.perform({ + query: /* GraphQL */ ` + query { + some: __typename + } + `, + }); }); it('runs context building for operation containing non introspection fields', async () => { const testInstance = createTestkit( @@ -36,16 +40,18 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - query { - __schema { - aaa: __typename - } - me { - id + await testInstance.perform({ + query: /* GraphQL */ ` + query { + __schema { + aaa: __typename + } + me { + id + } } - } - `); + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -62,13 +68,15 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - mutation { - createUser { - id + await testInstance.perform({ + query: /* GraphQL */ ` + mutation { + createUser { + id + } } - } - `); + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -85,11 +93,13 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - subscription { - message - } - `); + await testInstance.perform({ + query: /* GraphQL */ ` + subscription { + message + } + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -106,16 +116,18 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - query { - __schema { - aaa: __typename - } - me { - id + await testInstance.perform({ + query: /* GraphQL */ ` + query { + __schema { + aaa: __typename + } + me { + id + } } - } - `); + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -134,6 +146,6 @@ describe('useImmediateIntrospection', () => { schema ); - await testInstance.execute(getIntrospectionQuery()); + await testInstance.perform({ query: getIntrospectionQuery() }); }); }); diff --git a/packages/plugins/live-query/README.md b/packages/plugins/live-query/README.md index 990cf39750..3ba0688d77 100644 --- a/packages/plugins/live-query/README.md +++ b/packages/plugins/live-query/README.md @@ -27,6 +27,7 @@ yarn add @envelop/live-query @n1ru4l/in-memory-live-query-store ### `makeExecutableSchema` from `graphql-tools` ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop, useSchema, useExtendContext } from '@envelop/core' import { useLiveQuery } from '@envelop/live-query' import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store' @@ -59,6 +60,10 @@ setInterval(() => { }, 1000) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useLiveQuery({ liveQueryStore }), diff --git a/packages/plugins/live-query/test/use-live-query.spec.ts b/packages/plugins/live-query/test/use-live-query.spec.ts index 64dd1cf0de..4697afecef 100644 --- a/packages/plugins/live-query/test/use-live-query.spec.ts +++ b/packages/plugins/live-query/test/use-live-query.spec.ts @@ -27,13 +27,14 @@ describe('useLiveQuery', () => { const contextValue = { greetings: ['Hi', 'Sup', 'Ola'], }; - const result = await testKit.execute( - /* GraphQL */ ` - query @live { - greetings - } - `, - undefined, + const result = await testKit.perform( + { + query: /* GraphQL */ ` + query @live { + greetings + } + `, + }, contextValue ); assertStreamExecutionValue(result); @@ -77,13 +78,14 @@ describe('useLiveQuery', () => { const contextValue = { greetings: ['Hi', 'Sup', 'Ola'], }; - const result = await testKit.execute( - /* GraphQL */ ` - query @live { - greetings - } - `, - undefined, + const result = await testKit.perform( + { + query: /* GraphQL */ ` + query @live { + greetings + } + `, + }, contextValue ); assertStreamExecutionValue(result); diff --git a/packages/plugins/newrelic/README.md b/packages/plugins/newrelic/README.md index d98218ea10..75867c60ce 100644 --- a/packages/plugins/newrelic/README.md +++ b/packages/plugins/newrelic/README.md @@ -31,10 +31,15 @@ yarn add newrelic @envelop/newrelic ## Basic usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useNewRelic } from '@envelop/newrelic' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useNewRelic({ diff --git a/packages/plugins/newrelic/package.json b/packages/plugins/newrelic/package.json index 541df1537a..013a0b0d61 100644 --- a/packages/plugins/newrelic/package.json +++ b/packages/plugins/newrelic/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/packages/plugins/newrelic/src/index.ts b/packages/plugins/newrelic/src/index.ts index e3ceeb4e19..840512bc42 100644 --- a/packages/plugins/newrelic/src/index.ts +++ b/packages/plugins/newrelic/src/index.ts @@ -1,4 +1,5 @@ -import { Plugin, OnResolverCalledHook, Path, isAsyncIterable, EnvelopError, DefaultContext } from '@envelop/core'; +import { Plugin, Path, isAsyncIterable, DefaultContext } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { print, FieldNode, Kind, OperationDefinitionNode, ExecutionResult, GraphQLError } from 'graphql'; enum AttributeName { @@ -29,7 +30,7 @@ export type UseNewRelicOptions = { extractOperationName?: (context: DefaultContext) => string | undefined; /** * Indicates whether or not to skip reporting a given error to NewRelic. - * By default, this plugin skips all `EnvelopError` errors and does not report them to NewRelic. + * By default, this plugin skips all `Error` errors and does not report them to NewRelic. */ skipError?: (error: GraphQLError) => boolean; }; @@ -46,13 +47,9 @@ const DEFAULT_OPTIONS: UseNewRelicOptions = { trackResolvers: false, includeResolverArgs: false, rootFieldsNaming: false, - skipError: defaultSkipError, + skipError: () => false, }; -export function defaultSkipError(error: GraphQLError): boolean { - return error.originalError instanceof EnvelopError; -} - export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => { const options: InternalOptions = { ...DEFAULT_OPTIONS, @@ -81,12 +78,60 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => { }); return { + onPluginInit({ addPlugin }) { + if (options.trackResolvers) { + addPlugin( + useOnResolve(async ({ args: resolversArgs, info }) => { + const instrumentationApi = await instrumentationApi$; + const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState; + const delimiter = transactionNameState.delimiter; + + const logger = await logger$; + const { returnType, path, parentType } = info; + const formattedPath = flattenPath(path, delimiter); + const currentSegment = instrumentationApi.getActiveSegment(); + if (!currentSegment) { + logger.trace('No active segment found at resolver call. Not recording resolver (%s).', formattedPath); + return () => {}; + } + + const resolverSegment = instrumentationApi.createSegment( + `resolver${delimiter}${formattedPath}`, + null, + currentSegment + ); + if (!resolverSegment) { + logger.trace('Resolver segment was not created (%s).', formattedPath); + return () => {}; + } + resolverSegment.start(); + resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath); + resolverSegment.addAttribute(AttributeName.RESOLVER_TYPE_NAME, parentType.toString()); + resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT_TYPE, returnType.toString()); + if (options.includeResolverArgs) { + const rawArgs = resolversArgs || {}; + const resolverArgsToTrack = options.isResolverArgsRegex + ? filterPropertiesByRegex(rawArgs, options.includeResolverArgs as RegExp) + : rawArgs; + resolverSegment.addAttribute(AttributeName.RESOLVER_ARGS, JSON.stringify(resolverArgsToTrack)); + } + return ({ result }) => { + if (options.includeRawResult) { + resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT, JSON.stringify(result)); + } + resolverSegment.end(); + }; + }) + ); + } + }, async onExecute({ args }) { const instrumentationApi = await instrumentationApi$; const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState; const spanContext = instrumentationApi.agent.tracer.getSpanContext(); const delimiter = transactionNameState.delimiter; const rootOperation = args.document.definitions.find( + // @ts-expect-error TODO: not sure how we will make it dev friendly definitionNode => definitionNode.kind === Kind.OPERATION_DEFINITION ) as OperationDefinitionNode; const operationType = rootOperation.operation; @@ -128,56 +173,7 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => { const operationSegment = instrumentationApi.getActiveSegment(); - const onResolverCalled: OnResolverCalledHook | undefined = options.trackResolvers - ? async ({ args: resolversArgs, info }) => { - const logger = await logger$; - const { returnType, path, parentType } = info; - const formattedPath = flattenPath(path, delimiter); - const currentSegment = instrumentationApi.getActiveSegment(); - - if (!currentSegment) { - logger.trace('No active segment found at resolver call. Not recording resolver (%s).', formattedPath); - return () => {}; - } - - const resolverSegment = instrumentationApi.createSegment( - `resolver${delimiter}${formattedPath}`, - null, - operationSegment - ); - - if (!resolverSegment) { - logger.trace('Resolver segment was not created (%s).', formattedPath); - return () => {}; - } - - resolverSegment.start(); - - resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath); - resolverSegment.addAttribute(AttributeName.RESOLVER_TYPE_NAME, parentType.toString()); - resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT_TYPE, returnType.toString()); - - if (options.includeResolverArgs) { - const rawArgs = resolversArgs || {}; - const resolverArgsToTrack = options.isResolverArgsRegex - ? filterPropertiesByRegex(rawArgs, options.includeResolverArgs as RegExp) - : rawArgs; - - resolverSegment.addAttribute(AttributeName.RESOLVER_ARGS, JSON.stringify(resolverArgsToTrack)); - } - - return ({ result }) => { - if (options.includeRawResult) { - resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT, JSON.stringify(result)); - } - - resolverSegment.end(); - }; - } - : undefined; - return { - onResolverCalled, onExecuteDone({ result }) { const sendResult = (singularResult: ExecutionResult) => { if (singularResult.data && options.includeRawResult) { diff --git a/packages/plugins/on-resolve/README.md b/packages/plugins/on-resolve/README.md new file mode 100644 index 0000000000..326065b65b --- /dev/null +++ b/packages/plugins/on-resolve/README.md @@ -0,0 +1,98 @@ +## `@envelop/on-resolve` + +This plugin allows you to hook into resolves of every field in the GraphQL schema. + +Useful for tracing or augmenting resolvers (and their results) with custom logic. + +## Getting Started + +``` +yarn add @envelop/on-resolve +``` + +## Usage Example + +### Custom field resolutions + +```ts +import { envelop } from '@envelop/core' +import { useOnResolve } from '@envelop/on-resolve' +import { specialResolver } from './my-resolvers' + +const getEnveloped = envelop({ + plugins: [ + // ... other plugins ... + useOnResolve(async function onResolve({ context, root, args, info, replaceResolver }) { + // replace special field's resolver + if (info.fieldName === 'special') { + replaceResolver(specialResolver) + } + + // replace field's result + if (info.fieldName === 'alwaysHello') { + return ({ setResult }) => { + setResult('hello') + } + } + }) + ] +}) +``` + +### Tracing + +```ts +import { envelop, Plugin } from '@envelop/core' +import { useOnResolve } from '@envelop/on-resolve' + +interface FieldTracingPluginContext { + tracerUrl: string +} + +function useFieldTracing() { + return { + onPluginInit({ addPlugin }) { + addPlugin( + useOnResolve(async function onResolve({ context, root, args, info }) { + await fetch(context.tracerUrl, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + startedResolving: { + ...info, + parent: root, + args + } + }) + }) + + return async () => { + await fetch(context.tracerUrl, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + endedResolving: { + ...info, + parent: root, + args + } + }) + }) + } + }) + ) + } + } +} + +const getEnveloped = envelop({ + plugins: [ + // ... other plugins ... + useSpecialResolve() + ] +}) +``` diff --git a/packages/plugins/on-resolve/package.json b/packages/plugins/on-resolve/package.json new file mode 100644 index 0000000000..1e931c13d2 --- /dev/null +++ b/packages/plugins/on-resolve/package.json @@ -0,0 +1,66 @@ +{ + "name": "@envelop/on-resolve", + "version": "1.0.0", + "author": "Denis Badurina ", + "license": "MIT", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/n1ru4l/envelop.git", + "directory": "packages/plugins/on-resolve" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./*": { + "require": { + "types": "./dist/typings/*.d.cts", + "default": "./dist/cjs/*.js" + }, + "import": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + }, + "default": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "dependencies": {}, + "devDependencies": { + "graphql": "16.3.0", + "typescript": "4.7.4" + }, + "peerDependencies": { + "@envelop/core": "^2.5.0", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "type": "module" +} diff --git a/packages/plugins/on-resolve/src/index.ts b/packages/plugins/on-resolve/src/index.ts new file mode 100644 index 0000000000..73b2ed9976 --- /dev/null +++ b/packages/plugins/on-resolve/src/index.ts @@ -0,0 +1,85 @@ +import { defaultFieldResolver, GraphQLResolveInfo, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; +import { Plugin, PromiseOrValue } from '@envelop/core'; + +export type Resolver = ( + root: unknown, + args: Record, + context: Context, + info: GraphQLResolveInfo +) => PromiseOrValue; + +export type AfterResolver = (options: { + result: unknown; + setResult: (newResult: unknown) => void; +}) => PromiseOrValue; + +export interface OnResolveOptions = {}> { + context: PluginContext; + root: unknown; + args: Record; + info: GraphQLResolveInfo; + resolver: Resolver; + replaceResolver: (newResolver: Resolver) => void; +} + +export type OnResolve = {}> = ( + options: OnResolveOptions +) => PromiseOrValue; + +/** + * Wraps the provided schema by hooking into the resolvers of every field. + * + * Use the `onResolve` argument to manipulate the resolver and its results/errors. + */ +export function useOnResolve = {}>( + onResolve: OnResolve +): Plugin { + return { + onSchemaChange({ schema: _schema }) { + const schema = _schema as GraphQLSchema; + if (!schema) return; // nothing to do if schema is missing + + for (const type of Object.values(schema.getTypeMap())) { + if (!isIntrospectionType(type) && isObjectType(type)) { + for (const field of Object.values(type.getFields())) { + let resolver = (field.resolve || defaultFieldResolver) as Resolver; + + field.resolve = async (root, args, context, info) => { + const afterResolve = await onResolve({ + root, + args, + context, + info, + resolver, + replaceResolver: newResolver => { + resolver = newResolver; + }, + }); + + let result; + try { + result = await resolver(root, args, context, info); + } catch (err) { + result = err as Error; + } + + if (typeof afterResolve === 'function') { + await afterResolve({ + result, + setResult: newResult => { + result = newResult; + }, + }); + } + + if (result instanceof Error) { + throw result; + } + return result; + }; + } + } + } + }, + }; +} diff --git a/packages/plugins/on-resolve/test/use-on-resolve.spec.ts b/packages/plugins/on-resolve/test/use-on-resolve.spec.ts new file mode 100644 index 0000000000..f16f165d68 --- /dev/null +++ b/packages/plugins/on-resolve/test/use-on-resolve.spec.ts @@ -0,0 +1,62 @@ +import { OnResolveOptions, useOnResolve } from '@envelop/on-resolve'; +import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('useOnResolve', () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + value1: String! + value2: String! + } + `, + resolvers: { + Query: { + value1: () => 'value1', + value2: () => 'value2', + }, + }, + }); + + it('should invoke the callback for each resolver', async () => { + const onResolveDoneFn = jest.fn(); + const onResolveFn = jest.fn((_opts: OnResolveOptions) => onResolveDoneFn); + const testkit = createTestkit([useOnResolve(onResolveFn)], schema); + + await testkit.perform({ query: '{ value1, value2 }' }); + + expect(onResolveFn).toBeCalledTimes(2); + expect(onResolveDoneFn).toBeCalledTimes(2); + + let i = 0; + for (const field of ['value1', 'value2']) { + expect(onResolveFn.mock.calls[i][0].context).toBeDefined(); + expect(onResolveFn.mock.calls[i][0].args).toBeDefined(); + expect(onResolveFn.mock.calls[i][0].info).toBeDefined(); + expect(onResolveFn.mock.calls[i][0].info.fieldName).toBe(field); + expect(onResolveFn.mock.calls[i][0].resolver).toBeInstanceOf(Function); + expect(onResolveFn.mock.calls[i][0].replaceResolver).toBeInstanceOf(Function); + + expect(onResolveDoneFn.mock.calls[i][0].result).toBe(field); + expect(onResolveDoneFn.mock.calls[i][0].setResult).toBeInstanceOf(Function); + + i++; + } + }); + + it('should replace the result using the after hook', async () => { + const testkit = createTestkit( + [ + useOnResolve(() => ({ setResult }) => { + setResult('value2'); + }), + ], + schema + ); + + const result = await testkit.perform({ query: '{ value1 }' }); + assertSingleExecutionValue(result); + + expect(result.data?.value1).toBe('value2'); + }); +}); diff --git a/packages/plugins/opentelemetry/README.md b/packages/plugins/opentelemetry/README.md index 7b94d8dc5f..f46a3ec180 100644 --- a/packages/plugins/opentelemetry/README.md +++ b/packages/plugins/opentelemetry/README.md @@ -15,10 +15,15 @@ yarn add @envelop/opentelemetry By default, this plugin prints the collected telemetry to the console: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useOpenTelemetry } from '@envelop/opentelemetry' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useOpenTelemetry({ @@ -33,6 +38,7 @@ const getEnveloped = envelop({ If you wish to use custom tracer/exporter, create it and pass it. This example integrates Jaeger tracer: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useOpenTelemetry } from '@envelop/opentelemetry' import { JaegerExporter } from '@opentelemetry/exporter-jaeger' @@ -47,6 +53,10 @@ provider.addSpanProcessor(new SimpleSpanProcessor(exporter)) provider.register() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useOpenTelemetry( diff --git a/packages/plugins/opentelemetry/package.json b/packages/plugins/opentelemetry/package.json index 3028d79f04..2d784dd4f6 100644 --- a/packages/plugins/opentelemetry/package.json +++ b/packages/plugins/opentelemetry/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "@opentelemetry/api": "^1.0.0", "@opentelemetry/tracing": "^0.24.0", "tslib": "^2.4.0" diff --git a/packages/plugins/opentelemetry/src/index.ts b/packages/plugins/opentelemetry/src/index.ts index 2e2a6b56fb..362d34c47e 100644 --- a/packages/plugins/opentelemetry/src/index.ts +++ b/packages/plugins/opentelemetry/src/index.ts @@ -1,4 +1,5 @@ import { Plugin, OnExecuteHookResult, isAsyncIterable } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { SpanAttributes, SpanKind } from '@opentelemetry/api'; import * as opentelemetry from '@opentelemetry/api'; import { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; @@ -45,41 +46,45 @@ export const useOpenTelemetry = ( const tracer = tracingProvider.getTracer(serviceName); return { - onResolverCalled: options.resolvers - ? ({ info, context, args }) => { - if (context && typeof context === 'object' && context[tracingSpanSymbol]) { - tracer.getActiveSpanProcessor(); - const ctx = opentelemetry.trace.setSpan(opentelemetry.context.active(), context[tracingSpanSymbol]); - const { fieldName, returnType, parentType } = info; - - const resolverSpan = tracer.startSpan( - `${parentType.name}.${fieldName}`, - { - attributes: { - [AttributeName.RESOLVER_FIELD_NAME]: fieldName, - [AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(), - [AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(), - [AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}), + onPluginInit({ addPlugin }) { + if (options.resolvers) { + addPlugin( + useOnResolve(({ info, context, args }) => { + if (context && typeof context === 'object' && context[tracingSpanSymbol]) { + tracer.getActiveSpanProcessor(); + const ctx = opentelemetry.trace.setSpan(opentelemetry.context.active(), context[tracingSpanSymbol]); + const { fieldName, returnType, parentType } = info; + + const resolverSpan = tracer.startSpan( + `${parentType.name}.${fieldName}`, + { + attributes: { + [AttributeName.RESOLVER_FIELD_NAME]: fieldName, + [AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(), + [AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(), + [AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}), + }, }, - }, - ctx - ); - - return ({ result }) => { - if (result instanceof Error) { - resolverSpan.recordException({ - name: AttributeName.RESOLVER_EXCEPTION, - message: JSON.stringify(result), - }); - } else { - resolverSpan.end(); - } - }; - } - - return () => {}; - } - : undefined, + ctx + ); + + return ({ result }) => { + if (result instanceof Error) { + resolverSpan.recordException({ + name: AttributeName.RESOLVER_EXCEPTION, + message: JSON.stringify(result), + }); + } else { + resolverSpan.end(); + } + }; + } + + return () => {}; + }) + ); + } + }, onExecute({ args, extendContext }) { const executionSpan = tracer.startSpan(`${args.operationName || 'Anonymous Operation'}`, { kind: spanKind, diff --git a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts index fecebd3d8f..9f51fadf28 100644 --- a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts +++ b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts @@ -46,7 +46,7 @@ describe('useOpenTelemetry', () => { schema ); - const result = await testInstance.execute(query); + const result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(onExecuteSpy).toHaveBeenCalledTimes(1); }); @@ -55,7 +55,7 @@ describe('useOpenTelemetry', () => { const exporter = new InMemorySpanExporter(); const testInstance = createTestkit([useTestOpenTelemetry(exporter)], schema); - await testInstance.execute(query); + await testInstance.perform({ query }); const actual = exporter.getFinishedSpans(); expect(actual.length).toBe(1); expect(actual[0].name).toBe('Anonymous Operation'); @@ -65,7 +65,7 @@ describe('useOpenTelemetry', () => { const exporter = new InMemorySpanExporter(); const testInstance = createTestkit([useTestOpenTelemetry(exporter, { resolvers: true })], schema); - await testInstance.execute(query); + await testInstance.perform({ query }); const actual = exporter.getFinishedSpans(); expect(actual.length).toBe(2); expect(actual[0].name).toBe('Query.ping'); diff --git a/packages/plugins/operation-field-permissions/README.md b/packages/plugins/operation-field-permissions/README.md index 6bd673f814..05066c8875 100644 --- a/packages/plugins/operation-field-permissions/README.md +++ b/packages/plugins/operation-field-permissions/README.md @@ -13,10 +13,15 @@ yarn add @envelop/operation-field-permissions ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop, useSchema } from '@envelop/core' import { useOperationFieldPermissions } from '@envelop/operation-field-permissions' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useOperationFieldPermissions({ diff --git a/packages/plugins/operation-field-permissions/src/index.ts b/packages/plugins/operation-field-permissions/src/index.ts index 3d6d4d3df7..dc2473c2b3 100644 --- a/packages/plugins/operation-field-permissions/src/index.ts +++ b/packages/plugins/operation-field-permissions/src/index.ts @@ -1,4 +1,4 @@ -import { EnvelopError, Plugin, useExtendContext } from '@envelop/core'; +import { Plugin, useExtendContext } from '@envelop/core'; import { ExtendedValidationRule, useExtendedValidation } from '@envelop/extended-validation'; import { isUnionType, @@ -8,6 +8,7 @@ import { isInterfaceType, isIntrospectionType, getNamedType, + GraphQLError, } from 'graphql'; type PromiseOrValue = T | Promise; @@ -63,10 +64,9 @@ const OperationScopeRule = !permissionContext.wildcardTypes.has(objectType.name) && !permissionContext.schemaCoordinates.has(schemaCoordinate) ) { - // TODO: EnvelopError was a bad idea ;) // We should use GraphQLError once the object constructor lands in stable GraphQL.js // and useMaskedErrors supports it. - const error = new EnvelopError(options.formatError(schemaCoordinate)); + const error = new GraphQLError(options.formatError(schemaCoordinate)); (error as any).nodes = [node]; context.reportError(error); } @@ -124,7 +124,9 @@ type OperationScopeOptions = { const defaultFormatError = (schemaCoordinate: string) => `Insufficient permissions for selecting '${schemaCoordinate}'.`; -export const useOperationFieldPermissions = (opts: OperationScopeOptions): Plugin => { +export const useOperationFieldPermissions = ( + opts: OperationScopeOptions +): Plugin<{ [OPERATION_PERMISSIONS_SYMBOL]: ScopeContext }> => { return { onPluginInit({ addPlugin }) { addPlugin( diff --git a/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts b/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts index 93655b8aea..8c84615e59 100644 --- a/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts +++ b/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts @@ -2,7 +2,6 @@ import { useOperationFieldPermissions } from '../src/index.js'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; import { getIntrospectionQuery } from 'graphql'; -import { useMaskedErrors } from '@envelop/core'; const schema = makeExecutableSchema({ typeDefs: [ @@ -53,7 +52,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(getIntrospectionQuery()); + const result = await kit.perform({ query: getIntrospectionQuery() }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -68,14 +67,16 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - __schema { - __typename + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + __schema { + __typename + } + greetings } - greetings - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -94,7 +95,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -109,7 +110,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -129,7 +130,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -147,7 +148,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -161,13 +162,15 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - postOrUser { - __typename + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + postOrUser { + __typename + } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -188,13 +191,15 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - node { - __typename + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + node { + __typename + } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -215,34 +220,16 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - __typename - } - `); + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + __typename + } + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); const [error] = result.errors!; expect(error.nodes).toBeDefined(); }); - - it('is not masked by the masked errors plugin', async () => { - const kit = createTestkit( - [ - useOperationFieldPermissions({ - getPermissions: () => new Set([]), - }), - useMaskedErrors(), - ], - schema - ); - const result = await kit.execute(/* GraphQL */ ` - query { - __typename - } - `); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors![0].message).toEqual("Insufficient permissions for selecting 'Query.__typename'."); - }); }); diff --git a/packages/plugins/parser-cache/README.md b/packages/plugins/parser-cache/README.md index db0666321f..d39cab4b64 100644 --- a/packages/plugins/parser-cache/README.md +++ b/packages/plugins/parser-cache/README.md @@ -13,10 +13,15 @@ yarn add @envelop/parser-cache ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useParserCache } from '@envelop/parser-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useParserCache({ diff --git a/packages/plugins/parser-cache/test/parser-cache.spec.ts b/packages/plugins/parser-cache/test/parser-cache.spec.ts index 68deb1f71e..eb3606f6fc 100644 --- a/packages/plugins/parser-cache/test/parser-cache.spec.ts +++ b/packages/plugins/parser-cache/test/parser-cache.spec.ts @@ -30,23 +30,23 @@ describe('useParserCache', () => { it('Should call original parse when cache is empty', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); expect(testParser).toHaveBeenCalledTimes(1); }); it('Should call parse once once when operation is cached', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); expect(testParser).toHaveBeenCalledTimes(1); }); it('Should call parse once once when operation is cached and errored', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - const r1 = await testInstance.execute(`FAILED\ { foo }`); + const r1 = await testInstance.perform({ query: `FAILED\ { foo }` }); assertSingleExecutionValue(r1); - const r2 = await testInstance.execute(`FAILED\ { foo }`); + const r2 = await testInstance.perform({ query: `FAILED\ { foo }` }); assertSingleExecutionValue(r2); expect(testParser).toHaveBeenCalledTimes(1); expect(r1.errors![0].message).toBe(`Syntax Error: Unexpected Name "FAILED".`); @@ -56,8 +56,8 @@ describe('useParserCache', () => { it('Should call parse multiple times on different operations', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - await testInstance.execute(`query t { foo }`); - await testInstance.execute(`query t2 { foo }`); + await testInstance.perform({ query: `query t { foo }` }); + await testInstance.perform({ query: `query t2 { foo }` }); expect(testParser).toHaveBeenCalledTimes(2); }); @@ -75,9 +75,9 @@ describe('useParserCache', () => { ], testSchema ); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); await testInstance.wait(10); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); expect(testParser).toHaveBeenCalledTimes(2); }); @@ -95,7 +95,7 @@ describe('useParserCache', () => { testSchema ); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); expect(documentCache.get).toHaveBeenCalled(); expect(documentCache.set).toHaveBeenCalled(); }); @@ -114,7 +114,7 @@ describe('useParserCache', () => { testSchema ); - await testInstance.execute(`FAILED\ { foo }`); + await testInstance.perform({ query: `FAILED\ { foo }` }); expect(errorCache.get).toHaveBeenCalled(); expect(errorCache.set).toHaveBeenCalled(); }); diff --git a/packages/plugins/persisted-operations/README.md b/packages/plugins/persisted-operations/README.md index 5f25519e52..9536163f7c 100644 --- a/packages/plugins/persisted-operations/README.md +++ b/packages/plugins/persisted-operations/README.md @@ -15,6 +15,7 @@ yarn add @envelop/persisted-operations The most basic implementation can use an in-memory JS `Map` wrapper with a `Store` object: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { usePersistedOperations, InMemoryStore } from '@envelop/persisted-operations' @@ -28,6 +29,10 @@ const store = new InMemoryStore({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ @@ -58,6 +63,8 @@ usePersistedOperations({ ## Usage Example with built-in JsonFileStore ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' import { usePersistedOperations, JsonFileStore } from '@envelop/persisted-operations' const persistedOperationsStore = new JsonFilesStore() @@ -70,6 +77,10 @@ persistedOperationsStore.loadFromFileSync(filePath) // load and parse persisted- await persistedOperationsStore.loadFromFile(filePath) // load and parse persisted-operations files const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ @@ -84,7 +95,14 @@ const getEnveloped = envelop({ The `store` parameter accepts both a `Store` instance, or a function. If you need to support multiple stores (based on incoming GraphQL operation/HTTP request), you can provide a function to toggle between the stores, based on your needs: ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ diff --git a/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts b/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts index 75ce4f6f49..517d1d6634 100644 --- a/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts +++ b/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts @@ -30,7 +30,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`persisted_1`, {}, {}); + const result = await testInstance.perform({ query: `persisted_1` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.foo).toBe('test'); @@ -49,7 +49,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`persisted_1`); + const result = await testInstance.perform({ query: `persisted_1` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe(`Unable to match operation with id 'persisted_1'`); }); @@ -67,7 +67,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`persisted_2`); + const result = await testInstance.perform({ query: `persisted_2` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe(`Unable to match operation with id 'persisted_2'`); }); @@ -85,7 +85,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`invalid`); + const result = await testInstance.perform({ query: `invalid` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe(`Syntax Error: Unexpected Name "invalid".`); }); @@ -103,7 +103,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.foo).toBe('test'); @@ -122,7 +122,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe('Must provide operation id'); }); @@ -139,7 +139,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1'); + const result = await testInstance.perform({ query: 'persisted_1' }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe('Must provide store for persisted-operations!'); }); @@ -158,7 +158,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1', {}, initialContext); + const result = await testInstance.perform({ query: 'persisted_1' }, initialContext); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('test'); }); @@ -177,7 +177,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('invalid', {}, initialContext); + const result = await testInstance.perform({ query: 'invalid' }, initialContext); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('test'); }); @@ -196,7 +196,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`query { bar }`, {}, initialContext); + const result = await testInstance.perform({ query: `query { bar }` }, initialContext); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('test'); }); @@ -216,7 +216,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1', {}, initialContext); + const result = await testInstance.perform({ query: 'persisted_1' }, initialContext); assertSingleExecutionValue(result); expect(mockOnMissingMatch).toHaveBeenCalledTimes(1); expect(mockOnMissingMatch).toHaveBeenCalledWith(initialContext, 'persisted_1'); @@ -237,7 +237,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1', {}, initialContext); + const result = await testInstance.perform({ query: 'persisted_1' }, initialContext); assertSingleExecutionValue(result); expect(mockOnMissingMatch).toHaveBeenCalledTimes(1); expect(mockOnMissingMatch).toHaveBeenCalledWith(initialContext, 'persisted_1'); @@ -257,7 +257,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1'); + const result = await testInstance.perform({ query: 'persisted_1' }); assertSingleExecutionValue(result); expect(mockOnMissingMatch).not.toHaveBeenCalled(); }); @@ -276,7 +276,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1'); + const result = await testInstance.perform({ query: 'persisted_1' }); assertSingleExecutionValue(result); expect(mockOnMissingMatch).not.toHaveBeenCalled(); }); diff --git a/packages/plugins/preload-assets/README.md b/packages/plugins/preload-assets/README.md index b3d46d0e36..50fd866652 100644 --- a/packages/plugins/preload-assets/README.md +++ b/packages/plugins/preload-assets/README.md @@ -12,6 +12,7 @@ yarn add @envelop/preload-assets ``` ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { usePreloadAssets } from '@envelop/preload-asset' import { makeExecutableSchema } from 'graphql' @@ -34,6 +35,10 @@ const schema = makeExecutableSchema({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [usePreloadAssets()] }) ``` diff --git a/packages/plugins/preload-assets/package.json b/packages/plugins/preload-assets/package.json index 86c62dba83..ec6e19e8aa 100644 --- a/packages/plugins/preload-assets/package.json +++ b/packages/plugins/preload-assets/package.json @@ -54,8 +54,7 @@ "typescript": "4.7.4" }, "peerDependencies": { - "@envelop/core": "^2.6.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "@envelop/core": "^2.6.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/plugins/preload-assets/test/use-preload-assets.spec.ts b/packages/plugins/preload-assets/test/use-preload-assets.spec.ts index 5287e86a1f..1adb816b66 100644 --- a/packages/plugins/preload-assets/test/use-preload-assets.spec.ts +++ b/packages/plugins/preload-assets/test/use-preload-assets.spec.ts @@ -20,7 +20,7 @@ describe('usePreloadAssets', () => { it('Should include assets to preload', async () => { const testInstance = createTestkit([usePreloadAssets()], schema); - const result = await testInstance.execute(`query { imageUrl }`); + const result = await testInstance.perform({ query: `query { imageUrl }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -30,7 +30,7 @@ describe('usePreloadAssets', () => { it('Should not include the preload extension if no asset should be preloaded', async () => { const testInstance = createTestkit([usePreloadAssets()], schema); - const result = await testInstance.execute(`query { noAsset }`); + const result = await testInstance.perform({ query: `query { noAsset }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toBeUndefined(); @@ -44,7 +44,7 @@ describe('usePreloadAssets', () => { ], schema ); - const result = await testInstance.execute(`query { imageUrl }`); + const result = await testInstance.perform({ query: `query { imageUrl }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toBeUndefined(); diff --git a/packages/plugins/prometheus/README.md b/packages/plugins/prometheus/README.md index c182ca0762..6583158ec9 100644 --- a/packages/plugins/prometheus/README.md +++ b/packages/plugins/prometheus/README.md @@ -26,10 +26,15 @@ yarn add prom-client @envelop/prometheus ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { usePrometheus } from '@envelop/prometheus' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePrometheus({ @@ -57,11 +62,17 @@ const getEnveloped = envelop({ You can customize the `prom-client` `Registry` object if you are using a custom one, by passing it along with the configuration object: ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' import { Registry } from 'prom-client' const myRegistry = new Registry() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePrometheus({ @@ -83,11 +94,16 @@ If you wish to disable introspection logging, you can use `skipIntrospection: tr Each tracing field supports custom `prom-client` objects, and custom `labels` a metadata, you can create a custom extraction function for every `Histogram` / `Summary` / `Counter`: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { Histogram } from 'prom-client' import { envelop } from '@envelop/core' import { createHistogram, usePrometheus } from '@envelop/prometheus' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePrometheus({ diff --git a/packages/plugins/prometheus/package.json b/packages/plugins/prometheus/package.json index af0c9644ff..83809478c7 100644 --- a/packages/plugins/prometheus/package.json +++ b/packages/plugins/prometheus/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/packages/plugins/prometheus/src/index.ts b/packages/plugins/prometheus/src/index.ts index 97c7faee0e..73c72857cb 100644 --- a/packages/plugins/prometheus/src/index.ts +++ b/packages/plugins/prometheus/src/index.ts @@ -9,6 +9,7 @@ import { isIntrospectionOperationString, isAsyncIterable, } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { TypeInfo } from 'graphql'; import { Summary, Counter, Histogram, register as defaultRegistry } from 'prom-client'; import { @@ -324,31 +325,35 @@ export const usePrometheus = (config: PrometheusTracingPluginConfig = {}): Plugi : undefined; return { - onResolverCalled: resolversHistogram - ? ({ info, context }) => { - const shouldTrace = shouldTraceFieldResolver(info, config.resolversWhitelist); - - if (!shouldTrace) { - return undefined; - } - - const startTime = Date.now(); - - return () => { - const totalTime = (Date.now() - startTime) / 1000; - const paramsCtx = { - ...context[promPluginContext], - info, - }; - resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); - }; - } - : undefined, onEnveloped({ extendContext }) { extendContext({ [promPluginExecutionStartTimeSymbol]: Date.now(), }); }, + onPluginInit({ addPlugin }) { + if (resolversHistogram) { + addPlugin( + useOnResolve(({ info, context }) => { + const shouldTrace = shouldTraceFieldResolver(info, config.resolversWhitelist); + + if (!shouldTrace) { + return undefined; + } + + const startTime = Date.now(); + + return () => { + const totalTime = (Date.now() - startTime) / 1000; + const paramsCtx = { + ...context[promPluginContext], + info, + }; + resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); + }; + }) + ); + } + }, onSchemaChange({ schema }) { typeInfo = new TypeInfo(schema); }, diff --git a/packages/plugins/rate-limiter/README.md b/packages/plugins/rate-limiter/README.md index 8aeeed98be..4cb201849c 100644 --- a/packages/plugins/rate-limiter/README.md +++ b/packages/plugins/rate-limiter/README.md @@ -11,6 +11,7 @@ yarn add @envelop/rate-limiter ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useRateLimiter, IdentifyFn } from '@envelop/rate-limiter' @@ -19,6 +20,10 @@ const identifyFn: IdentifyFn = async context => { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useRateLimiter({ diff --git a/packages/plugins/rate-limiter/package.json b/packages/plugins/rate-limiter/package.json index 72a572bb88..e90d106e05 100644 --- a/packages/plugins/rate-limiter/package.json +++ b/packages/plugins/rate-limiter/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "graphql-rate-limit": "3.3.0", "tslib": "^2.4.0" }, diff --git a/packages/plugins/rate-limiter/src/index.ts b/packages/plugins/rate-limiter/src/index.ts index 25dac08c4c..3ee6023d5c 100644 --- a/packages/plugins/rate-limiter/src/index.ts +++ b/packages/plugins/rate-limiter/src/index.ts @@ -1,4 +1,5 @@ import { Plugin } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { IntValueNode, StringValueNode, GraphQLResolveInfo } from 'graphql'; import { getDirective } from './utils.js'; import { getGraphQLRateLimiter } from 'graphql-rate-limit'; @@ -19,61 +20,66 @@ export type RateLimiterPluginOptions = { onRateLimitError?: (event: { error: string; identifier: string; context: unknown; info: GraphQLResolveInfo }) => void; }; -export const useRateLimiter = ( - options: RateLimiterPluginOptions -): Plugin<{ +interface RateLimiterContext { rateLimiterFn: ReturnType; -}> => { +} + +export const useRateLimiter = (options: RateLimiterPluginOptions): Plugin => { const rateLimiterFn = getGraphQLRateLimiter({ identifyContext: options.identifyFn }); return { - async onContextBuilding({ extendContext }) { - extendContext({ - rateLimiterFn, - }); - }, - async onResolverCalled({ args, root, context, info }) { - const rateLimitDirectiveNode = getDirective(info, options.rateLimitDirectiveName || 'rateLimit'); + onPluginInit({ addPlugin }) { + addPlugin( + useOnResolve(async ({ args, root, context, info }) => { + const rateLimitDirectiveNode = getDirective(info, options.rateLimitDirectiveName || 'rateLimit'); - if (rateLimitDirectiveNode && rateLimitDirectiveNode.arguments) { - const maxNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'max')?.value as IntValueNode; - const windowNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'window') - ?.value as StringValueNode; - const messageNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'message') - ?.value as IntValueNode; + if (rateLimitDirectiveNode && rateLimitDirectiveNode.arguments) { + const maxNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'max') + ?.value as IntValueNode; + const windowNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'window') + ?.value as StringValueNode; + const messageNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'message') + ?.value as IntValueNode; - const message = messageNode.value; - const max = parseInt(maxNode.value); - const window = windowNode.value; - const id = options.identifyFn(context); + const message = messageNode.value; + const max = parseInt(maxNode.value); + const window = windowNode.value; + const id = options.identifyFn(context); - const errorMessage = await context.rateLimiterFn( - { parent: root, args, context, info }, - { - max, - window, - message: interpolate(message, { - id, - }), - } - ); - if (errorMessage) { - if (options.onRateLimitError) { - options.onRateLimitError({ - error: errorMessage, - identifier: id, - context, - info, - }); - } + const errorMessage = await context.rateLimiterFn( + { parent: root, args, context, info }, + { + max, + window, + message: interpolate(message, { + id, + }), + } + ); + if (errorMessage) { + if (options.onRateLimitError) { + options.onRateLimitError({ + error: errorMessage, + identifier: id, + context, + info, + }); + } - if (options.transformError) { - throw options.transformError(errorMessage); - } + if (options.transformError) { + throw options.transformError(errorMessage); + } - throw new Error(errorMessage); - } - } + throw new Error(errorMessage); + } + } + }) + ); + }, + async onContextBuilding({ extendContext }) { + extendContext({ + rateLimiterFn, + }); }, }; }; diff --git a/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts b/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts index 1fe9f8a41e..8a09cb6e19 100644 --- a/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts +++ b/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts @@ -11,7 +11,7 @@ describe('useRateLimiter', () => { const schemaWithDirective = makeExecutableSchema({ typeDefs: ` ${DIRECTIVE_SDL} - + type Query { limited: String @rateLimit( max: 1, @@ -39,9 +39,9 @@ describe('useRateLimiter', () => { schemaWithDirective ); - testInstance.execute(`query { unlimited }`); - await testInstance.execute(`query { unlimited }`); - const result = await testInstance.execute(`query { unlimited }`); + testInstance.perform({ query: `query { unlimited }` }); + await testInstance.perform({ query: `query { unlimited }` }); + const result = await testInstance.perform({ query: `query { unlimited }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.unlimited).toBe('unlimited'); @@ -57,9 +57,9 @@ describe('useRateLimiter', () => { schemaWithDirective ); - await testInstance.execute(`query { limited }`); + await testInstance.perform({ query: `query { limited }` }); await delay(300); - const result = await testInstance.execute(`query { limited }`); + const result = await testInstance.perform({ query: `query { limited }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.limited).toBe('limited'); @@ -74,8 +74,8 @@ describe('useRateLimiter', () => { ], schemaWithDirective ); - await testInstance.execute(`query { limited }`); - const result = await testInstance.execute(`query { limited }`); + await testInstance.perform({ query: `query { limited }` }); + const result = await testInstance.perform({ query: `query { limited }` }); assertSingleExecutionValue(result); expect(result.errors!.length).toBe(1); expect(result.errors![0].message).toBe('too many calls'); @@ -86,7 +86,7 @@ describe('useRateLimiter', () => { const schema = makeExecutableSchema({ typeDefs: ` ${DIRECTIVE_SDL} - + type Query { limited: String @rateLimit( max: 1, @@ -112,8 +112,8 @@ describe('useRateLimiter', () => { ], schema ); - await testInstance.execute(`query { limited }`); - const result = await testInstance.execute(`query { limited }`); + await testInstance.perform({ query: `query { limited }` }); + const result = await testInstance.perform({ query: `query { limited }` }); assertSingleExecutionValue(result); diff --git a/packages/plugins/resource-limitations/README.md b/packages/plugins/resource-limitations/README.md index d047f895df..8e72088dad 100644 --- a/packages/plugins/resource-limitations/README.md +++ b/packages/plugins/resource-limitations/README.md @@ -11,10 +11,15 @@ yarn add @envelop/resource-limitations ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResourceLimitations } from '@envelop/resource-limitations' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResourceLimitations({ diff --git a/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts b/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts index e9bd777487..b69dd11b2a 100644 --- a/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts +++ b/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts @@ -57,19 +57,21 @@ const schema = makeExecutableSchema({ describe('useResourceLimitations', () => { it('requires the usage of either the first or last field on fields that resolve to a Connection type.', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -79,19 +81,21 @@ describe('useResourceLimitations', () => { }); it('requires the usage of either the first or last field on fields that resolve to a Connection type (other argument provided).', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(after: "abc") { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(after: "abc") { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -101,19 +105,21 @@ describe('useResourceLimitations', () => { }); it('requires the first field to be at least 1', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 0) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 0) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -123,19 +129,21 @@ describe('useResourceLimitations', () => { }); it('requires the first field to be at least a custom minimum value', async () => { const testkit = createTestkit([useResourceLimitations({ paginationArgumentMinimum: 2, extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 1) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 1) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -145,19 +153,21 @@ describe('useResourceLimitations', () => { }); it('requires the first field to be not higher than 100', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 101) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 101) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -170,19 +180,21 @@ describe('useResourceLimitations', () => { [useResourceLimitations({ paginationArgumentMaximum: 99, extensions: true })], schema ); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -192,19 +204,21 @@ describe('useResourceLimitations', () => { }); it('requires the last field to be at least 1', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 0) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 0) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -214,19 +228,21 @@ describe('useResourceLimitations', () => { }); it('requires the last field to be at least a custom minimum value', async () => { const testkit = createTestkit([useResourceLimitations({ paginationArgumentMinimum: 2, extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 1) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 1) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -236,19 +252,21 @@ describe('useResourceLimitations', () => { }); it('requires the last field to be not higher than 100', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 101) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 101) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -261,19 +279,21 @@ describe('useResourceLimitations', () => { [useResourceLimitations({ paginationArgumentMaximum: 99, extensions: true })], schema ); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -283,19 +303,21 @@ describe('useResourceLimitations', () => { }); it('calculates node cost (single)', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -309,19 +331,21 @@ describe('useResourceLimitations', () => { [useResourceLimitations({ paginationArgumentScalars: ['ConnectionInt'], extensions: true })], schema ); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositoriesCustom(first: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositoriesCustom(first: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -332,18 +356,20 @@ describe('useResourceLimitations', () => { }); it('calculates node cost (nested)', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name - issues(first: 10) { - edges { - node { - title - bodyHTML + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + issues(first: 10) { + edges { + node { + title + bodyHTML + } } } } @@ -351,8 +377,8 @@ describe('useResourceLimitations', () => { } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -363,50 +389,52 @@ describe('useResourceLimitations', () => { }); it('calculates node cost (multiple nested)', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name - issues(first: 10) { - edges { - node { - title - bodyHTML + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + issues(first: 10) { + edges { + node { + title + bodyHTML + } } } } } } - } - more: repositories(last: 1) { - edges { - node { - name - issues(first: 2) { - edges { - node { - title - bodyHTML + more: repositories(last: 1) { + edges { + node { + name + issues(first: 2) { + edges { + node { + title + bodyHTML + } } } } } } - } - # These should not count towards the total due to invalid argument types - repositoriesCustom(first: 100) { - edges { - node { - name + # These should not count towards the total due to invalid argument types + repositoriesCustom(first: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -417,18 +445,20 @@ describe('useResourceLimitations', () => { }); it('stops execution if node cost limit is exceeded', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true, nodeCostLimit: 20 })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 19) { - edges { - node { - name - issues(first: 2) { - edges { - node { - title - bodyHTML + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 19) { + edges { + node { + name + issues(first: 2) { + edges { + node { + title + bodyHTML + } } } } @@ -436,8 +466,8 @@ describe('useResourceLimitations', () => { } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ resourceLimitations: { @@ -451,13 +481,15 @@ describe('useResourceLimitations', () => { }); it('minimum cost is always 1', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true, nodeCostLimit: 20 })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - id + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + id + } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ resourceLimitations: { diff --git a/packages/plugins/response-cache-redis/README.md b/packages/plugins/response-cache-redis/README.md index 98b8f5fb8b..62844e05d2 100644 --- a/packages/plugins/response-cache-redis/README.md +++ b/packages/plugins/response-cache-redis/README.md @@ -22,6 +22,7 @@ In order to use the Redis cache, you need to: - Create an instance of the Redis Cache and set to the `useResponseCache` plugin options ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -44,6 +45,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ cache }) @@ -54,6 +59,7 @@ const getEnveloped = envelop({ ### Invalidate Cache based on custom logic ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -66,6 +72,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ diff --git a/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts b/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts index b80feb87f2..d96cec08a9 100644 --- a/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts +++ b/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts @@ -87,8 +87,8 @@ describe('useResponseCache with Redis cache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); }); @@ -166,24 +166,24 @@ describe('useResponseCache with Redis cache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute( - /* GraphQL */ ` + await testInstance.perform({ + query: /* GraphQL */ ` mutation test($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -262,9 +262,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // get from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // so queried just once expect(spy).toHaveBeenCalledTimes(1); @@ -312,11 +312,11 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.exists('Comment:2')).toBeFalsy(); // query and cache since ws invalidated - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // but since we've queried once before when we started above, we've now actually queried twice expect(spy).toHaveBeenCalledTimes(2); }); @@ -396,9 +396,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // queried once expect(spy).toHaveBeenCalledTimes(1); @@ -419,16 +419,16 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.smembers('Comment')).toHaveLength(0); // we've invalidated so, now query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // so have queried twice expect(spy).toHaveBeenCalledTimes(2); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // still just queried twice expect(spy).toHaveBeenCalledTimes(2); @@ -512,29 +512,29 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - const queryResult = await testInstance.execute(query); + const queryResult = await testInstance.perform({ query }); let cacheHitMaybe = queryResult['extensions']['responseCache']['hit']; expect(cacheHitMaybe).toBeFalsy(); // get from cache - const cachedResult = await testInstance.execute(query); + const cachedResult = await testInstance.perform({ query }); cacheHitMaybe = cachedResult['extensions']['responseCache']['hit']; expect(cacheHitMaybe).toBeTruthy(); - const mutationResult = await testInstance.execute( - /* GraphQL */ ` + const mutationResult = await testInstance.perform({ + query: /* GraphQL */ ` mutation test($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); cacheHitMaybe = mutationResult['extensions']['responseCache']['hit']; expect(cacheHitMaybe).toBeFalsy(); @@ -604,18 +604,18 @@ describe('useResponseCache with Redis cache', () => { schema ); - const result = await testInstance.execute( - /* GraphQL */ ` + const result = await testInstance.perform({ + query: /* GraphQL */ ` mutation test($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); const responseCache = result['extensions']['responseCache']; const invalidatedEntities = responseCache['invalidatedEntities']; @@ -695,9 +695,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache 2 users - await testInstance.execute(query, { limit: 2 }); + await testInstance.perform({ query, variables: { limit: 2 } }); // fetch 2 users from cache - await testInstance.execute(query, { limit: 2 }); + await testInstance.perform({ query, variables: { limit: 2 } }); // so just one query expect(spy).toHaveBeenCalledTimes(1); @@ -705,7 +705,7 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.keys('operations:*')).toHaveLength(1); // query just one user - await testInstance.execute(query, { limit: 1 }); + await testInstance.perform({ query, variables: { limit: 1 } }); // since 2 users are in cache, we query again for the 1 as a response expect(spy).toHaveBeenCalledTimes(2); @@ -778,9 +778,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // so queried just once expect(spy).toHaveBeenCalledTimes(1); @@ -788,7 +788,7 @@ describe('useResponseCache with Redis cache', () => { jest.advanceTimersByTime(150); // since the cache has expired, now when we query - await testInstance.execute(query); + await testInstance.perform({ query }); // we query again so now twice expect(spy).toHaveBeenCalledTimes(2); }); @@ -867,16 +867,14 @@ describe('useResponseCache with Redis cache', () => { } `; - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } ); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } @@ -886,9 +884,8 @@ describe('useResponseCache with Redis cache', () => { // we should have one response for that sessionId of 1 expect(await redis.keys('operations:*')).toHaveLength(1); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 2, } @@ -967,7 +964,7 @@ describe('useResponseCache with Redis cache', () => { `; // query but don't cache - await testInstance.execute(query); + await testInstance.perform({ query }); // none of the queries entities are cached because contains Comment expect(await redis.exists('User')).toBeFalsy(); @@ -976,7 +973,7 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.exists('Comment:2')).toBeFalsy(); // since not cached - await testInstance.execute(query); + await testInstance.perform({ query }); // still none of the queries entities are cached because contains Comment expect(await redis.exists('User')).toBeFalsy(); @@ -1065,15 +1062,15 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); // wait so User expires jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); // now we've queried twice expect(spy).toHaveBeenCalledTimes(2); }); @@ -1156,15 +1153,15 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); // wait so User expires jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); // now we've queried twice expect(spy).toHaveBeenCalledTimes(2); }); diff --git a/packages/plugins/response-cache/README.md b/packages/plugins/response-cache/README.md index c6cc036e70..145b1ee605 100644 --- a/packages/plugins/response-cache/README.md +++ b/packages/plugins/response-cache/README.md @@ -38,10 +38,15 @@ When configuring the `useResponseCache`, you can choose the type of cache: The in-memory LRU cache is used by default. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -55,12 +60,17 @@ const getEnveloped = envelop({ Or, you may create the in-memory LRU cache explicitly. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, createInMemoryCache } from '@envelop/response-cache' const cache = createInMemoryCache() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -76,10 +86,15 @@ const getEnveloped = envelop({ ### Cache based on session/user ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -105,6 +120,7 @@ In order to use the Redis cache, you need to: - Create an instance of the Redis Cache and set to the `useResponseCache` plugin options ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -122,6 +138,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -139,10 +159,15 @@ const getEnveloped = envelop({ ### Cache with maximum TTL ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -158,10 +183,15 @@ const getEnveloped = envelop({ ### Cache with custom TTL per object type ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -178,10 +208,15 @@ const getEnveloped = envelop({ ### Cache with custom TTL per schema coordinate ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -198,10 +233,15 @@ const getEnveloped = envelop({ ### Disable cache based on session/user ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -228,6 +268,7 @@ cache results with certain error types. By default, the `defaultShouldCacheResult` function is used which never caches any query operation execution results that includes any errors (unexpected, EnvelopError, or GraphQLError). ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, ShouldCacheResultFunction } from '@envelop/response-cache' @@ -238,6 +279,10 @@ export const defaultShouldCacheResult: ShouldCacheResultFunction = (params): Boo } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -255,10 +300,15 @@ By default introspection query operations are not cached. In case you want to ca **Infinite caching** ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -274,10 +324,15 @@ const getEnveloped = envelop({ **TTL caching** ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -293,10 +348,15 @@ const getEnveloped = envelop({ ### Cache with maximum TTL ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -310,10 +370,15 @@ const getEnveloped = envelop({ ### Customize the fields that are used for building the cache ID ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -329,10 +394,15 @@ const getEnveloped = envelop({ ### Disable automatic cache invalidation via mutations ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -348,6 +418,7 @@ const getEnveloped = envelop({ ### Invalidate Cache based on custom logic ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, createInMemoryCache } from '@envelop/response-cache' import { emitter } from './eventEmitter' @@ -356,6 +427,10 @@ import { emitter } from './eventEmitter' const cache = createInMemoryCache() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -380,6 +455,7 @@ emitter.on('invalidate', resource => { ### Customize how cache ids are built ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, createInMemoryCache } from '@envelop/response-cache' import { emitter } from './eventEmitter' @@ -391,6 +467,10 @@ const cache = createInMemoryCache({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -408,7 +488,14 @@ const getEnveloped = envelop({ For debugging or monitoring it might be useful to know whether a response got served from the cache or not. ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ diff --git a/packages/plugins/response-cache/test/response-cache.spec.ts b/packages/plugins/response-cache/test/response-cache.spec.ts index 5aefe450b6..c8f9717295 100644 --- a/packages/plugins/response-cache/test/response-cache.spec.ts +++ b/packages/plugins/response-cache/test/response-cache.spec.ts @@ -120,13 +120,13 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -186,8 +186,8 @@ describe('useResponseCache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); }); @@ -269,30 +269,30 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(query); + let result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: true }); expect(spy).toHaveBeenCalledTimes(1); - result = await testInstance.execute( - /* GraphQL */ ` + result = await testInstance.perform({ + query: /* GraphQL */ ` mutation it($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); assertSingleExecutionValue(result); expect(result?.extensions?.responseCache).toEqual({ invalidatedEntities: [{ id: '1', typename: 'User' }] }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); expect(spy).toHaveBeenCalledTimes(2); @@ -382,30 +382,30 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(query); + let result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: true }); expect(spy).toHaveBeenCalledTimes(1); - result = await testInstance.execute( - /* GraphQL */ ` + result = await testInstance.perform({ + query: /* GraphQL */ ` mutation it($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); assertSingleExecutionValue(result); expect(result?.extensions?.responseCache).toEqual({ invalidatedEntities: [{ id: '1', typename: 'User' }] }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); expect(spy).toHaveBeenCalledTimes(2); @@ -486,13 +486,13 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); cache.invalidate([{ typename: 'Comment', id: 2 }]); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -571,13 +571,13 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); cache.invalidate([{ typename: 'Comment' }]); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -647,10 +647,10 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query, { limit: 2 }); - await testInstance.execute(query, { limit: 2 }); + await testInstance.perform({ query, variables: { limit: 2 } }); + await testInstance.perform({ query, variables: { limit: 2 } }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute(query, { limit: 1 }); + await testInstance.perform({ query, variables: { limit: 1 } }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -720,14 +720,14 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); // let's travel in time beyond the ttl of 100 jest.advanceTimersByTime(150); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -804,24 +804,21 @@ describe('useResponseCache', () => { } `; - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } ); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } ); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 2, } @@ -893,8 +890,8 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -974,11 +971,11 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1058,11 +1055,11 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1130,8 +1127,8 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1257,7 +1254,7 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(userQuery); + let result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1266,7 +1263,7 @@ describe('useResponseCache', () => { ttl: 200, }, }); - result = await testInstance.execute(orderQuery); + result = await testInstance.perform({ query: orderQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1277,12 +1274,12 @@ describe('useResponseCache', () => { }); jest.advanceTimersByTime(2); - await testInstance.execute(userQuery); - await testInstance.execute(orderQuery); + await testInstance.perform({ query: userQuery }); + await testInstance.perform({ query: orderQuery }); expect(userSpy).toHaveBeenCalledTimes(1); expect(orderSpy).toHaveBeenCalledTimes(2); jest.advanceTimersByTime(201); - await testInstance.execute(userQuery); + await testInstance.perform({ query: userQuery }); expect(userSpy).toHaveBeenCalledTimes(2); }); @@ -1363,7 +1360,7 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(userQuery); + let result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1374,7 +1371,7 @@ describe('useResponseCache', () => { }); jest.advanceTimersByTime(2); - result = await testInstance.execute(userQuery); + result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1384,7 +1381,7 @@ describe('useResponseCache', () => { jest.advanceTimersByTime(200); - result = await testInstance.execute(userQuery); + result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1428,10 +1425,10 @@ describe('useResponseCache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(4); }); @@ -1477,10 +1474,10 @@ describe('useResponseCache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); // the resolver is only called once as all following executions hit the cache expect(spy).toHaveBeenCalledTimes(1); }); @@ -1568,12 +1565,12 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute( - /* GraphQL */ ` + await testInstance.perform({ + query: /* GraphQL */ ` mutation it($id: ID!) { updateUser(id: $id) { id @@ -1581,14 +1578,14 @@ describe('useResponseCache', () => { } } `, - { + variables: { id: 1, - } - ); + }, + }); expect(errorSpy).toHaveBeenCalledTimes(1); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1634,10 +1631,10 @@ describe('useResponseCache', () => { // after each execution the introspectionCounter should be incremented by 1 // as we never cache the introspection - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); expect(introspectionCounter).toEqual(1); - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); expect(introspectionCounter).toEqual(2); }); @@ -1683,11 +1680,11 @@ describe('useResponseCache', () => { schema ); - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); // after the first execution the introspectionCounter should be incremented by 1 expect(introspectionCounter).toEqual(1); - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); // as we now cache the introspection the resolver shall not be called for further introspections expect(introspectionCounter).toEqual(1); }); @@ -1728,11 +1725,11 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); + await testInstance.perform({ query }); expect(usersResolverInvocationCount).toEqual(1); const testInstance2 = createTestkit([useResponseCache({ session: () => null, cache })], schema); - await testInstance2.execute(query); + await testInstance2.perform({ query }); expect(usersResolverInvocationCount).toEqual(2); }); @@ -1758,7 +1755,7 @@ describe('useResponseCache', () => { } `; - let result = await testkit.execute(document); + let result = await testkit.perform({ query: document }); expect(result).toMatchInlineSnapshot(` Object { "data": Object { @@ -1766,7 +1763,7 @@ describe('useResponseCache', () => { }, } `); - result = await testkit.execute(document); + result = await testkit.perform({ query: document }); expect(result).toMatchInlineSnapshot(` Object { "data": Object { @@ -1801,9 +1798,9 @@ describe('useResponseCache', () => { foo } `; - const result1 = await testkit.execute(operation); + const result1 = await testkit.perform({ query: operation }); assertSingleExecutionValue(result1); - const result2 = await testkit.execute(operation); + const result2 = await testkit.perform({ query: operation }); assertSingleExecutionValue(result2); // ensure the response is served from the cache expect(result1).toBe(result2); @@ -1831,18 +1828,20 @@ describe('useResponseCache', () => { }, }); const testkit = createTestkit([useResponseCache({ session: () => null })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - user { - __typename - id - friends { + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + user { __typename id + friends { + __typename + id + } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1877,16 +1876,18 @@ describe('useResponseCache', () => { }, }); const testkit = createTestkit([useResponseCache({ session: () => null })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - user { - id - friends { + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + user { id + friends { + id + } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1917,17 +1918,19 @@ describe('useResponseCache', () => { }, }); const testkit = createTestkit([useResponseCache({ session: () => null })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - user { - foo: __typename - id - friends { + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + user { + foo: __typename id + friends { + id + } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1983,7 +1986,7 @@ describe('useResponseCache', () => { schema ); - let result = await testkit.execute(operation); + let result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1999,13 +2002,13 @@ describe('useResponseCache', () => { }, }, }); - result = await testkit.execute(operation); + result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result.extensions?.['responseCache']).toEqual({ hit: true, }); await cache.invalidate([{ typename: 'Cat', id: '1' }]); - result = await testkit.execute(operation); + result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result.extensions?.['responseCache']).toEqual({ didCache: true, diff --git a/packages/plugins/sentry/README.md b/packages/plugins/sentry/README.md index 9f95bce9fa..c7f882b77d 100644 --- a/packages/plugins/sentry/README.md +++ b/packages/plugins/sentry/README.md @@ -30,12 +30,17 @@ yarn add @sentry/node @sentry/tracing @envelop/sentry ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useSentry } from '@envelop/sentry' // do this only once in you entry file. import '@sentry/tracing' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useSentry({ diff --git a/packages/plugins/sentry/package.json b/packages/plugins/sentry/package.json index fdd89fce42..2d32cc473e 100644 --- a/packages/plugins/sentry/package.json +++ b/packages/plugins/sentry/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/packages/plugins/sentry/src/index.ts b/packages/plugins/sentry/src/index.ts index 38640ce26b..c3843585aa 100644 --- a/packages/plugins/sentry/src/index.ts +++ b/packages/plugins/sentry/src/index.ts @@ -1,10 +1,5 @@ -import { - Plugin, - OnResolverCalledHook, - EnvelopError, - handleStreamOrSingleExecutionResult, - OnExecuteDoneHookResultOnNextHook, -} from '@envelop/core'; +import { Plugin, handleStreamOrSingleExecutionResult, OnExecuteDoneHookResultOnNextHook } from '@envelop/core'; +import { OnResolve, useOnResolve } from '@envelop/on-resolve'; import * as Sentry from '@sentry/node'; import type { Span, TraceparentData } from '@sentry/types'; import { ExecutionArgs, GraphQLError, Kind, OperationDefinitionNode, print, responsePathAsArray } from 'graphql'; @@ -80,13 +75,13 @@ export type SentryPluginOptions = { skip?: (args: ExecutionArgs) => boolean; /** * Indicates whether or not to skip Sentry exception reporting for a given error. - * By default, this plugin skips all `EnvelopError` errors and does not report it to Sentry. + * By default, this plugin skips all `Error` errors and does not report it to Sentry. */ skipError?: (args: Error) => boolean; }; export function defaultSkipError(error: Error): boolean { - return error instanceof EnvelopError; + return error instanceof Error; } const sentryTracingSymbol = Symbol('sentryTracing'); @@ -123,7 +118,7 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { }); } - const onResolverCalled: OnResolverCalledHook | undefined = trackResolvers + const onResolve: OnResolve | undefined = trackResolvers ? ({ args: resolversArgs, info, context }) => { const { rootSpan, opName, operationType } = context[sentryTracingSymbol] as SentryTracingContext; if (rootSpan) { @@ -169,13 +164,18 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { : undefined; return { - onResolverCalled, + onPluginInit({ addPlugin }) { + if (onResolve) { + addPlugin(useOnResolve(onResolve)); + } + }, onExecute({ args, extendContext }) { if (skipOperation(args)) { return; } const rootOperation = args.document.definitions.find( + // @ts-expect-error TODO: not sure how we will make it dev friendly o => o.kind === Kind.OPERATION_DEFINITION ) as OperationDefinitionNode; const operationType = rootOperation.operation; @@ -244,7 +244,7 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { Sentry.configureScope(scope => options.configureScope!(args, scope)); } - if (onResolverCalled) { + if (onResolve) { const sentryContext: SentryTracingContext = { rootSpan, opName, @@ -289,7 +289,7 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { // Map index values in list to $index for better grouping of events. const errorPathWithIndex = (err.path ?? []) - .map(v => (typeof v === 'number' ? '$index' : v)) + .map((v: any) => (typeof v === 'number' ? '$index' : v)) .join(' > '); const eventId = Sentry.captureException(err, { diff --git a/packages/plugins/statsd/README.md b/packages/plugins/statsd/README.md index 107a203582..581545dd92 100644 --- a/packages/plugins/statsd/README.md +++ b/packages/plugins/statsd/README.md @@ -25,6 +25,7 @@ yarn add hot-shots @envelop/stats ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useStatsD } from '@envelop/statsd' import StatsD from 'hot-shots' @@ -35,6 +36,10 @@ const client = new StatsD({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useStatsD({ diff --git a/packages/plugins/statsd/package.json b/packages/plugins/statsd/package.json index f5de2c4382..ceb57e6ba2 100644 --- a/packages/plugins/statsd/package.json +++ b/packages/plugins/statsd/package.json @@ -63,7 +63,6 @@ }, "peerDependencies": { "@envelop/core": "^2.6.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", "hot-shots": "^8.0.0 || ^9.0.0" }, "buildOptions": { diff --git a/packages/plugins/statsd/src/index.ts b/packages/plugins/statsd/src/index.ts index 8a6316800d..d9f3fe3d15 100644 --- a/packages/plugins/statsd/src/index.ts +++ b/packages/plugins/statsd/src/index.ts @@ -1,5 +1,4 @@ import { Plugin, AfterParseEventPayload, isIntrospectionOperationString, isAsyncIterable } from '@envelop/core'; -import { DocumentNode, Kind, OperationDefinitionNode } from 'graphql'; import type { StatsD } from 'hot-shots'; export interface StatsDPluginOptions { @@ -31,8 +30,8 @@ interface PluginInternalContext { [statsDPluginExecutionStartTimeSymbol]: number; } -function getOperation(document: DocumentNode) { - return document.definitions.find(def => def.kind === Kind.OPERATION_DEFINITION) as OperationDefinitionNode; +function getOperation(document: any) { + return document.definitions.find((def: any) => def.kind === 'OperationDefinition'); } function isParseFailure(parseResult: AfterParseEventPayload['result']): parseResult is Error | null { diff --git a/packages/plugins/validation-cache/README.md b/packages/plugins/validation-cache/README.md index 9f2fec7501..db52e73d06 100644 --- a/packages/plugins/validation-cache/README.md +++ b/packages/plugins/validation-cache/README.md @@ -13,10 +13,15 @@ yarn add @envelop/validation-cache ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useValidationCache } from '@envelop/validation-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useValidationCache({ diff --git a/packages/plugins/validation-cache/src/index.ts b/packages/plugins/validation-cache/src/index.ts index acd655b962..d0bec4d43a 100644 --- a/packages/plugins/validation-cache/src/index.ts +++ b/packages/plugins/validation-cache/src/index.ts @@ -59,6 +59,7 @@ export const useValidationCache = (pluginOptions: ValidationCacheOptions = {}): } return ({ result }) => { + // @ts-expect-error TODO: not sure how we will make it dev friendly resultCache.set(key, result); }; }, diff --git a/packages/plugins/validation-cache/test/validation-cache.spec.ts b/packages/plugins/validation-cache/test/validation-cache.spec.ts index dee90b7b9f..a3a460e4ff 100644 --- a/packages/plugins/validation-cache/test/validation-cache.spec.ts +++ b/packages/plugins/validation-cache/test/validation-cache.spec.ts @@ -30,30 +30,30 @@ describe('useValidationCache', () => { it('Should call original validate when cache is empty', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); expect(testValidator).toHaveBeenCalledTimes(1); }); it('Should call validate once once when operation is cached', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); expect(testValidator).toHaveBeenCalledTimes(1); }); it('Should call validate once once when operation is cached and errored', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - const r1 = await testInstance.execute(`query { foo2 }`); - const r2 = await testInstance.execute(`query { foo2 }`); + const r1 = await testInstance.perform({ query: `query { foo2 }` }); + const r2 = await testInstance.perform({ query: `query { foo2 }` }); expect(testValidator).toHaveBeenCalledTimes(1); expect(r1).toEqual(r2); }); it('Should call validate multiple times on different operations', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - await testInstance.execute(`query t { foo }`); - await testInstance.execute(`query t2 { foo }`); + await testInstance.perform({ query: `query t { foo }` }); + await testInstance.perform({ query: `query t2 { foo }` }); expect(testValidator).toHaveBeenCalledTimes(2); }); @@ -71,9 +71,9 @@ describe('useValidationCache', () => { ], testSchema ); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); await testInstance.wait(10); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); expect(testValidator).toHaveBeenCalledTimes(2); }); @@ -90,8 +90,8 @@ describe('useValidationCache', () => { ], testSchema ); - await testInstance.execute(`query { foo2 }`); - await testInstance.execute(`query { foo2 }`); + await testInstance.perform({ query: `query { foo2 }` }); + await testInstance.perform({ query: `query { foo2 }` }); expect(cache.get).toHaveBeenCalled(); expect(cache.set).toHaveBeenCalled(); }); diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 1bfad5498b..370c0f76c4 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -1,6 +1,17 @@ -import { DocumentNode, ExecutionResult, getOperationAST, GraphQLError, GraphQLSchema, print } from 'graphql'; -import { useSchema, envelop, PluginOrDisabledPlugin, isAsyncIterable } from '@envelop/core'; -import { GetEnvelopedFn, Plugin } from '@envelop/types'; +import { + DocumentNode, + ExecutionResult, + getOperationAST, + GraphQLError, + GraphQLSchema, + print, + execute, + parse, + subscribe, + validate, +} from 'graphql'; +import { useSchema, envelop, isAsyncIterable } from '@envelop/core'; +import { GetEnvelopedFn, Plugin, PerformFunction } from '@envelop/types'; import { mapSchema as cloneSchema, isDocumentNode } from '@graphql-tools/utils'; export type ModifyPluginsFn = (plugins: Plugin[]) => Plugin[]; @@ -48,7 +59,6 @@ export function createSpiedPlugin() { beforeExecute: jest.fn(() => ({ onExecuteDone: baseSpies.afterExecute, })), - onResolverCalled: baseSpies.beforeResolver, }; return { @@ -64,7 +74,6 @@ export function createSpiedPlugin() { onValidate: spies.beforeValidate, onExecute: spies.beforeExecute, onContextBuilding: spies.beforeContextBuilding, - onResolverCalled: spies.beforeResolver, }, }; } @@ -75,18 +84,25 @@ type MaybeAsyncIterableIterator = T | AsyncIterableIterator; type ExecutionReturn = MaybeAsyncIterableIterator; export type TestkitInstance = { + perform: PerformFunction; + /** @deprecated Consider using `perform` instead. */ execute: ( operation: DocumentNode | string, variables?: Record, initialContext?: any ) => MaybePromise; modifyPlugins: (modifyPluginsFn: ModifyPluginsFn) => void; + /** + * Works only when used with `execute`, will NOT work with `perform`. + * + * @deprecated Consider using plugins for mocking. + */ mockPhase: (phaseReplacement: PhaseReplacementParams) => void; wait: (ms: number) => Promise; }; export function createTestkit( - pluginsOrEnvelop: GetEnvelopedFn | Array, + pluginsOrEnvelop: GetEnvelopedFn | Parameters['0']['plugins'], schema?: GraphQLSchema ): TestkitInstance { const toGraphQLErrorOrThrow = (thrownThing: unknown): GraphQLError => { @@ -101,6 +117,10 @@ export function createTestkit( let getEnveloped = Array.isArray(pluginsOrEnvelop) ? envelop({ plugins: [...(schema ? [useSchema(cloneSchema(schema))] : []), ...pluginsOrEnvelop], + parse, + execute, + validate, + subscribe, }) : pluginsOrEnvelop; @@ -108,12 +128,41 @@ export function createTestkit( modifyPlugins(modifyPluginsFn: ModifyPluginsFn) { getEnveloped = envelop({ plugins: [...(schema ? [useSchema(cloneSchema(schema))] : []), ...modifyPluginsFn(getEnveloped._plugins)], + parse, + execute, + validate, + subscribe, }); }, mockPhase(phaseReplacement: PhaseReplacementParams) { phasesReplacements.push(phaseReplacement); }, wait: ms => new Promise(resolve => setTimeout(resolve, ms)), + perform: (params, initialContext) => { + const { perform } = getEnveloped(initialContext as any); + return perform(params, { + request: { + headers: {}, + method: 'POST', + query: '', + body: { + query: params.query, + variables: params.variables, + }, + }, + // TODO: how important is the document object? + get document() { + try { + return parse(params.query!); + } catch { + return {}; + } + }, + operation: params.query, + variables: params.variables, + // ...initialContext unnecessary spread + }); + }, execute: async (operation, variableValues = {}, initialContext = {}) => { const proxy = getEnveloped(initialContext); diff --git a/packages/testing/test/test.spec.ts b/packages/testing/test/test.spec.ts index 42d3de89a6..8730c9740b 100644 --- a/packages/testing/test/test.spec.ts +++ b/packages/testing/test/test.spec.ts @@ -1,4 +1,3 @@ -import { enableIf } from '@envelop/core'; import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; import { Plugin } from '@envelop/types'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -83,7 +82,7 @@ describe('Test the testkit', () => { }; const testkit = createTestkit([], createSchema()); testkit.modifyPlugins(plugins => [addedPlugin]); - const result = await testkit.execute('query test { foo }'); + const result = await testkit.perform({ query: 'query test { foo }' }); assertSingleExecutionValue(result); expect(addedPlugin.onParse).toBeCalled(); expect(addedPlugin.onValidate).toBeCalled(); @@ -98,11 +97,22 @@ describe('Test the testkit', () => { onValidate: jest.fn().mockReturnValue(undefined), }; - const testkit = createTestkit([plugin1, enableIf(false, plugin2)], createSchema()); - const result = await testkit.execute('query test { foo }'); + const testkit = createTestkit([plugin1, false && plugin2], createSchema()); + const result = await testkit.perform({ query: 'query test { foo }' }); assertSingleExecutionValue(result); expect(plugin1.onParse).toBeCalled(); expect(plugin2.onValidate).not.toBeCalled(); expect(result.data).toBeDefined(); }); + + it('Should use perform', async () => { + const testkit = createTestkit([], createSchema()); + const result = await testkit.perform({ query: 'query test { foo }' }); + assertSingleExecutionValue(result); + expect(result.data).toMatchInlineSnapshot(` + Object { + "foo": "1", + } + `); + }); }); diff --git a/packages/types/package.json b/packages/types/package.json index 83d31e4ce1..d6485bb9c6 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -50,12 +50,8 @@ "tslib": "^2.4.0" }, "devDependencies": { - "graphql": "16.3.0", "typescript": "4.7.4" }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" - }, "buildOptions": { "input": "./src/index.ts" }, diff --git a/packages/types/src/get-enveloped.ts b/packages/types/src/get-enveloped.ts index 800f0a5cad..ad90b8b97f 100644 --- a/packages/types/src/get-enveloped.ts +++ b/packages/types/src/get-enveloped.ts @@ -1,7 +1,7 @@ import { Plugin } from './plugin.js'; -import { GraphQLSchema } from 'graphql'; -import { ExecuteFunction, ParseFunction, SubscribeFunction, ValidateFunction } from './graphql.js'; -import { ArbitraryObject, Spread, PromiseOrValue } from './utils.js'; +import { ExecuteFunction, ExecutionResult, ParseFunction, SubscribeFunction, ValidateFunction } from './graphql.js'; +import { ArbitraryObject, Spread, PromiseOrValue, AsyncIterableIteratorOrValue } from './utils.js'; +import { PerformParams } from './hooks.js'; export { ArbitraryObject } from './utils.js'; export type EnvelopContextFnWrapper = ( @@ -17,7 +17,19 @@ export type GetEnvelopedFn = { contextFactory: ( contextExtension?: ContextExtension ) => PromiseOrValue>; - schema: GraphQLSchema; + schema: any; + /** + * Parse, validate, assemble context and execute/subscribe. + * + * Returns a ready-to-use GraphQL response. + * + * This function will NEVER throw GraphQL errors, it will instead place them + * in the result. However, non-GraphQL errors WILL bubble if thrown. + */ + perform: ( + params: PerformParams, + contextExtension?: ContextExtension + ) => Promise>; }; _plugins: Plugin[]; }; diff --git a/packages/types/src/graphql.ts b/packages/types/src/graphql.ts index 9a8ac5e6ee..ef83bf5e46 100644 --- a/packages/types/src/graphql.ts +++ b/packages/types/src/graphql.ts @@ -1,48 +1,22 @@ -import type { - DocumentNode, - GraphQLFieldResolver, - GraphQLSchema, - SubscriptionArgs, - ExecutionArgs, - GraphQLTypeResolver, - subscribe, - execute, - parse, - validate, - GraphQLResolveInfo, -} from 'graphql'; -import type { Maybe } from './utils.js'; - -/** @private */ -export type PolymorphicExecuteArguments = - | [ExecutionArgs] - | [ - GraphQLSchema, - DocumentNode, - any, - any, - Maybe<{ [key: string]: any }>, - Maybe, - Maybe>, - Maybe> - ]; +import { ObjMap } from './utils.js'; +export interface ExecutionArgs { + schema: any; + document: any; + rootValue?: any; + contextValue?: any; + variableValues?: any; + operationName?: any; + fieldResolver?: any; + typeResolver?: any; + subscribeFieldResolver?: any; +} +declare function parse(source: any, options?: any): any; +declare function execute(args: ExecutionArgs): any; +declare function subscribe(args: ExecutionArgs): any; +declare function validate(schema: any, documentAST: any, rules?: any, options?: any, typeInfo?: any): any; export type ExecuteFunction = typeof execute; -/** @private */ -export type PolymorphicSubscribeArguments = - | [SubscriptionArgs] - | [ - GraphQLSchema, - DocumentNode, - any?, - any?, - Maybe<{ [key: string]: any }>?, - Maybe?, - Maybe>?, - Maybe>? - ]; - export type SubscribeFunction = typeof subscribe; export type ParseFunction = typeof parse; @@ -70,4 +44,31 @@ export type ValidateFunctionParameter = { options?: Parameters[4]; }; -export type Path = GraphQLResolveInfo['path']; +/** @private */ +export type PolymorphicExecuteArguments = + | [ExecutionArgs] + | [ + ExecutionArgs['schema'], + ExecutionArgs['document'], + ExecutionArgs['rootValue'], + ExecutionArgs['contextValue'], + ExecutionArgs['variableValues'], + ExecutionArgs['operationName'], + ExecutionArgs['fieldResolver'], + ExecutionArgs['typeResolver'] + ]; + +/** @private */ +export type PolymorphicSubscribeArguments = PolymorphicExecuteArguments; + +export type Path = { + readonly prev: Path | undefined; + readonly key: string | number; + readonly typename: string | undefined; +}; + +export interface ExecutionResult, TExtensions = ObjMap> { + errors?: ReadonlyArray; + data?: TData | null; + extensions?: TExtensions; +} diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index 5b47c1a09e..8dc370f0d8 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -1,34 +1,23 @@ -import type { - DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, - GraphQLResolveInfo, - GraphQLSchema, - ParseOptions, - Source, - SubscriptionArgs, - ValidationRule, -} from 'graphql'; import { Maybe, PromiseOrValue, AsyncIterableIteratorOrValue } from './utils.js'; -import { DefaultContext } from './context-types.js'; import { ExecuteFunction, ParseFunction, ValidateFunction, ValidateFunctionParameter, SubscribeFunction, + ExecutionResult, + ExecutionArgs, } from './graphql.js'; import { Plugin } from './plugin.js'; export type DefaultArgs = Record; -export type SetSchemaFn = (newSchema: GraphQLSchema) => void; +export type SetSchemaFn = (newSchema: any) => void; /** * The payload forwarded to the onSchemaChange hook. */ -export type OnSchemaChangeEventPayload = { schema: GraphQLSchema; replaceSchema: SetSchemaFn }; +export type OnSchemaChangeEventPayload = { schema: any; replaceSchema: SetSchemaFn }; /** * Invoked each time the schema is changed via a setSchema call. @@ -53,15 +42,15 @@ export type RegisterContextErrorHandler = (handler: OnContextErrorHandler) => vo /** * Payload forwarded to the onPluginInit hook. */ -export type OnPluginInitEventPayload = { +export type OnPluginInitEventPayload> = { /** * Register a new plugin. */ - addPlugin: (newPlugin: Plugin) => void; + addPlugin: (newPlugin: Plugin) => void; /** * A list of all currently active plugins. */ - plugins: Plugin[]; + plugins: Plugin[]; /** * Set the GraphQL schema. */ @@ -75,7 +64,9 @@ export type OnPluginInitEventPayload = { /** * Invoked when a plugin is initialized. */ -export type OnPluginInitHook = (options: OnPluginInitEventPayload) => void; +export type OnPluginInitHook> = ( + options: OnPluginInitEventPayload +) => void; /** onPluginInit */ export type OnEnvelopedHookEventPayload = { @@ -107,7 +98,7 @@ export type OnParseEventPayload = { /** * The parameters that are passed to the parse call. */ - params: { source: string | Source; options?: ParseOptions }; + params: { source: string | any; options?: any }; /** * The current parse function */ @@ -120,7 +111,7 @@ export type OnParseEventPayload = { * Set/overwrite the parsed document. * If a parsed document is set the call to the parseFn will be skipped. */ - setParsedDocument: (doc: DocumentNode) => void; + setParsedDocument: (doc: any) => void; }; export type AfterParseEventPayload = { @@ -135,11 +126,11 @@ export type AfterParseEventPayload = { /** * The result of the parse phase. */ - result: DocumentNode | Error | null; + result: any | Error | null; /** * Replace the parse result with a new result. */ - replaceParseResult: (newResult: DocumentNode | Error) => void; + replaceParseResult: (newResult: any | Error) => void; }; /** @@ -171,7 +162,7 @@ export type OnValidateEventPayload = { /** * Register a validation rule that will be used for the validate invocation. */ - addValidationRule: (rule: ValidationRule) => void; + addValidationRule: (rule: any) => void; /** * The current validate function that will be invoked. */ @@ -183,7 +174,7 @@ export type OnValidateEventPayload = { /** * Set a validation error result and skip the validate invocation. */ - setResult: (errors: readonly GraphQLError[]) => void; + setResult: (errors: readonly any[]) => void; }; /** @@ -206,11 +197,11 @@ export type AfterValidateEventPayload = { * An array of errors that were raised during the validation phase. * The array is empty if no errors were raised. */ - result: readonly GraphQLError[]; + result: readonly Error[] | any[]; /** * Replace the current error result with a new one. */ - setResult: (errors: GraphQLError[]) => void; + setResult: (errors: Error[] | any[]) => void; }; /** @@ -272,40 +263,6 @@ export type OnContextBuildingHook = ( options: OnContextBuildingEventPayload ) => PromiseOrValue>; -export type ResolverFn = ( - root: ParentType, - args: ArgsType, - context: ContextType, - info: GraphQLResolveInfo -) => PromiseOrValue; - -export type OnBeforeResolverCalledEventPayload< - ParentType = unknown, - ArgsType = DefaultArgs, - ContextType = unknown, - ResultType = unknown -> = { - root: ParentType; - args: ArgsType; - context: ContextType; - info: GraphQLResolveInfo; - resolverFn: ResolverFn; - replaceResolverFn: (newResolver: ResolverFn) => void; -}; - -export type AfterResolverEventPayload = { result: unknown | Error; setResult: (newResult: unknown) => void }; - -export type AfterResolverHook = (options: AfterResolverEventPayload) => void; - -export type OnResolverCalledHook< - ParentType = unknown, - ArgsType = DefaultArgs, - ContextType = DefaultContext, - ResultType = unknown -> = ( - options: OnBeforeResolverCalledEventPayload -) => PromiseOrValue; - /** * Execution arguments with inferred context value type. */ @@ -428,7 +385,7 @@ export type OnExecuteHook = ( /** * Subscription arguments with inferred context value type. */ -export type TypedSubscriptionArgs = Omit & { contextValue: ContextType }; +export type TypedSubscriptionArgs = Omit & { contextValue: ContextType }; /** * Payload with which the onSubscribe hook is invoked. @@ -545,3 +502,46 @@ export type OnSubscribeHookResult = { export type OnSubscribeHook = ( options: OnSubscribeEventPayload ) => PromiseOrValue>; + +export interface PerformParams { + operationName?: string; + query?: string; + variables?: Record; +} + +/** + * Performs the parsing, validation, context assembly and execution/subscription. + * + * Will never throw GraphQL errors, they will be constructed accordingly and placed in the result. + */ +export type PerformFunction = ( + params: PerformParams, + contextExtension?: ContextExtension +) => Promise>; + +export type OnPerformEventPayload = { + context: Readonly; + extendContext: (contextExtension: Partial) => void; + params: PerformParams; + setParams: (newParams: PerformParams) => void; + /** + * Set an early result which will be immediatelly returned. Useful for cached results. + */ + setResult: (newResult: AsyncIterableIteratorOrValue) => void; +}; + +export type OnPerformDoneEventPayload = { + context: Readonly; + result: AsyncIterableIteratorOrValue; + setResult: (newResult: AsyncIterableIteratorOrValue) => void; +}; + +export type OnPerformDoneHook = (options: OnPerformDoneEventPayload) => PromiseOrValue; + +export type OnPerformHookResult = { + onPerformDone?: OnPerformDoneHook; +}; + +export type OnPerformHook = ( + options: OnPerformEventPayload +) => PromiseOrValue>; diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index 37941b362d..5da2899c38 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -7,8 +7,7 @@ import { OnSchemaChangeHook, OnSubscribeHook, OnValidateHook, - OnResolverCalledHook, - DefaultArgs, + OnPerformHook, } from './hooks.js'; export interface Plugin = {}> { @@ -23,7 +22,7 @@ export interface Plugin = {}> { /** * Invoked when a plugin is initialized. */ - onPluginInit?: OnPluginInitHook; + onPluginInit?: OnPluginInitHook; /** * Invoked for each execute call. */ @@ -45,7 +44,7 @@ export interface Plugin = {}> { */ onContextBuilding?: OnContextBuildingHook; /** - * Invoked before each resolver has been invoked during the execution phase. + * Invoked for each perform call. */ - onResolverCalled?: OnResolverCalledHook; + onPerform?: OnPerformHook; } diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index 57920b749e..0faa5ef955 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -29,3 +29,20 @@ export type ArbitraryObject = Record; export type PromiseOrValue = T | Promise; export type AsyncIterableIteratorOrValue = T | AsyncIterableIterator; export type Maybe = T | null | undefined; +export type Optional = T | Maybe | false; +export interface ObjMap { + [key: string]: T; +} +export type ObjMapLike = + | ObjMap + | { + [key: string]: T; + }; +export interface ReadOnlyObjMap { + readonly [key: string]: T; +} +export type ReadOnlyObjMapLike = + | ReadOnlyObjMap + | { + readonly [key: string]: T; + }; diff --git a/tsconfig.json b/tsconfig.json index b9fd0057a7..0ecbe64115 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "importHelpers": true, "experimentalDecorators": true, "module": "node16", - "target": "es2018", + "target": "ES2020", "lib": ["es6", "esnext", "es2015", "dom"], "suppressImplicitAnyIndexErrors": true, "moduleResolution": "node", diff --git a/website/algolia-lockfile.json b/website/algolia-lockfile.json index 9e359f524a..61eb96b190 100644 --- a/website/algolia-lockfile.json +++ b/website/algolia-lockfile.json @@ -364,8 +364,8 @@ }, { "children": [], - "title": "`onResolverCalled(api)`", - "anchor": "onresolvercalledapi" + "title": "`onPerform(api)`", + "anchor": "onperformapi" } ], "title": "`onPluginInit(api)`", diff --git a/website/docs/README.mdx b/website/docs/README.mdx index de4ac8abd7..8cf5bf7c3d 100644 --- a/website/docs/README.mdx +++ b/website/docs/README.mdx @@ -18,7 +18,6 @@ Plugins allow hooking into all GraphQL phases such as `parse`, `validate`, `exec | Example Plugin | Description | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | [`useLogger`](/plugins/use-logger) | Hooks into "before" of all phases, and prints the execution parameters to the console using `console.log` | -| [`useTiming`](/plugins/use-timing) | Hooks into "before" and "after" of all phases, measures times, and then prints them | | [`useParserCache`](/plugins/use-parser-cache) | Hooks into "before" and "after" of the `parse` phase and implements caching based on the operation string | | [`useOpenTelemetry`](/plugins/use-open-telemetry) | Hooks into all phases, execution and resolvers, and creates Spans for OpenTelemetry performance tracing | diff --git a/website/docs/core.mdx b/website/docs/core.mdx index e0e796e5c2..d1532842c3 100644 --- a/website/docs/core.mdx +++ b/website/docs/core.mdx @@ -12,11 +12,15 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can s ```ts import { envelop, useSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const mySchema = buildSchema(/* ... */) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(mySchema) // ... other plugins ... @@ -24,37 +28,19 @@ const getEnveloped = envelop({ }) ``` -#### useAsyncSchema - -This plugin is the simplest plugin for specifying your GraphQL schema, but in an async way. - -If you are using a framework that creates the schema in an async way, you can either use this plugin, or `await` for the schema and then use `useSchema`. - -```ts -import { envelop, useAsyncSchema } from '@envelop/core' -import { buildSchema } from 'graphql' - -const getSchema = async (): Promise => { - // return schema when it's ready -} - -const getEnveloped = envelop({ - plugins: [ - useAsyncSchema(getSchema()) - // ... other plugins ... - ] -}) -``` - #### useErrorHandler This plugin invokes a custom function with the every time execution encounters an error. ```ts import { envelop, useErrorHandler } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useErrorHandler(error => { // This callback is called per each GraphQLError emitted during execution phase @@ -72,9 +58,13 @@ Easily extends the context with custom fields. ```ts import { envelop, useExtendContext } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useExtendContext(async contextSoFar => { return { @@ -94,9 +84,13 @@ Logs parameters and information about the execution phases. You can easily plug ```ts import { envelop, useLogger } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useLogger({ logFn: (eventName, args) => { @@ -117,9 +111,13 @@ The second argument `executionArgs` provides additional information for your for ```ts import { envelop, usePayloadFormatter } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ usePayloadFormatter((result, executionArgs) => { // Return a modified result here, @@ -129,89 +127,3 @@ const getEnveloped = envelop({ ] }) ``` - -#### useTiming - -It's a simple time metric collection for every phase in your execution. You can easily customize the behavior of each timing measurement. By default, the timing is printed to the log, using `console.log`. - -```ts -import { envelop, useTiming } from '@envelop/core' -import { buildSchema } from 'graphql' - -const getEnveloped = envelop({ - plugins: [ - useTiming({ - // All options are optional. By default it just print it to the log. - // ResultTiming is an object built with { ms, ns } (milliseconds and nanoseconds) - onContextBuildingMeasurement: (timing: ResultTiming) => {}, - onExecutionMeasurement: (args: ExecutionArgs, timing: ResultTiming) => {}, - onSubscriptionMeasurement: (args: SubscriptionArgs, timing: ResultTiming) => {}, - onParsingMeasurement: (source: Source | string, timing: ResultTiming) => {}, - onValidationMeasurement: (document: DocumentNode, timing: ResultTiming) => {}, - onResolverMeasurement: (info: GraphQLResolveInfo, timing: ResultTiming) => {} - }) - // ... other plugins ... - ] -}) -``` - -#### useMaskedErrors - -Prevent unexpected error messages from leaking to the GraphQL API consumers. - -```ts -import { envelop, useSchema, useMaskedErrors, EnvelopError } from '@envelop/core' -import { makeExecutableSchema } from 'graphql' - -const schema = makeExecutableSchema({ - typeDefs: /* GraphQL */ ` - type Query { - something: String! - somethingElse: String! - somethingSpecial: String! - } - `, - resolvers: { - Query: { - something: () => { - throw new EnvelopError('Error that is propagated to the clients.') - }, - somethingElse: () => { - throw new Error("Unsafe error that will be masked as 'Unexpected Error.'.") - }, - somethingSpecial: () => { - throw new EnvelopError('The error will have an extensions field.', { - code: 'ERR_CODE', - randomNumber: 123 - }) - } - } - } -}) - -const getEnveloped = envelop({ - plugins: [useSchema(schema), useMaskedErrors()] -}) -``` - -### Utilities - -#### enableIf - -This utility is helpful when you want to enable a plugin only when a certain condition is met. - -```ts -import { envelop, useMaskedErrors, enableIf } from '@envelop/core' - -const isProd = process.env.NODE_ENV === 'production' - -const getEnveloped = envelop({ - plugins: [ - // This plugin is enabled only in production - enableIf(isProd, useMaskedErrors()), - // you can also pass function - enableIf(isProd, () => useMaskedErrors()) - // ... other plugins ... - ] -}) -``` diff --git a/website/docs/getting-started.mdx b/website/docs/getting-started.mdx index 0674119e46..a13cf7f0a5 100644 --- a/website/docs/getting-started.mdx +++ b/website/docs/getting-started.mdx @@ -17,6 +17,7 @@ Start by adding the core of `envelop` and `graphql` to your codebase. After installing the `@envelop/core` package, you can use the `envelop` function for creating your `getEnveloped` function. We use a simple GraphQL schema that we build with the `buildSchema` function from `graphql`. ```ts +import { parse, validate, execute, subcribe } from 'graphql' import { envelop, useSchema } from '@envelop/core' import { buildSchema } from 'graphql' @@ -27,16 +28,22 @@ const schema = buildSchema(/* GraphQL */ ` `) export const getEnveloped = envelop({ + parse, + validate, + execute, + subcribe, plugins: [useSchema(schema)] }) ``` ## Use your envelop -The result of `envelop` is a factory function that allows you to get everything you need for the GraphQL execution: `parse`, `validate`, `contextBuilder`, `execute` and `subscribe`. It is usually named `getEnveloped`. +The result of `envelop` is a factory function that allows you to get everything you need for the GraphQL execution: `parse`, `validate`, `contextBuilder`, `execute`, `subscribe` and `perform`. It is usually named `getEnveloped`. By calling the `getEnveloped` function you will get all the primitive functions required for the GraphQL execution layer. +It's recommended to use the `perform` function which does parsing, validation, context assembly and execution/subscription, and returns a ready result. This function will NEVER throw GraphQL errors, it will instead place them in the result. + ```ts // prettier-ignore const { @@ -46,6 +53,7 @@ const { execute, subscribe, schema, + perform, } = getEnveloped() ``` @@ -58,6 +66,7 @@ Let's add a parser and validation cache, so sending the same operation string se ```ts +import { parse, validate, execute, subcribe } from 'graphql' import { envelop, useSchema } from '@envelop/core' import { buildSchema } from 'graphql' import { useParserCache } from '@envelop/parser-cache' @@ -70,6 +79,10 @@ const schema = buildSchema(/* GraphQL */ ` `) const getEnveloped = envelop({ + parse, + validate, + execute, + subcribe, plugins: [ // all enabled plugins useSchema(schema), @@ -105,32 +118,16 @@ const httpServer = http.createServer(async (req, res) => { }) const { - // Get the GraphQL execution functions with attached plugin handlers - parse, - validate, - contextFactory, - execute, - schema + // Get the perform function that does parsing, validation, context assembly and execution/subscription, and returns a ready GraphQL operation result. + perform // pass in an initial context that all plugins can consume and extend } = getEnveloped({ req }) // Parse request body JSON const { query, variables } = JSON.parse(req.body) - const document = parse(query) - const validationErrors = validate(schema, document) - if (validationErrors.length > 0) { - return res.end(JSON.stringify({ errors: validationErrors })) - } - - // Build the context and execute - const contextValue = await contextFactory() - const result = await execute({ - document, - schema, - variableValues: variables, - contextValue - }) + // Perform the GraphQL operation. This function will NEVER throw GraphQL errors, it will instead place them in the result. + const result = await perform({ query, variables }) // Send the response res.end(JSON.stringify(result)) diff --git a/website/docs/guides/adding-a-graphql-response-cache.mdx b/website/docs/guides/adding-a-graphql-response-cache.mdx index 9675fa83d1..3dc8ec2e05 100644 --- a/website/docs/guides/adding-a-graphql-response-cache.mdx +++ b/website/docs/guides/adding-a-graphql-response-cache.mdx @@ -297,10 +297,15 @@ const getEnveloped = envelop({ Don't want to automatically invalidate based on mutations? Also configurable! ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -315,9 +320,7 @@ Want a global cache on Redis? Maybe you are in a server-less environment and the In-Memory Cache isn't an option. Also, when having multiple server replicas, you might want to have a shared cache between all the replicas. -```bash -yarn add @envelop/response-cache-redis -``` + First create a Redis database with your favorite hosting provider. @@ -328,6 +331,7 @@ Then, with that instance of the Redis Cache setup, provide it to the `useRespons Here's an example: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -344,6 +348,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ cache }) diff --git a/website/docs/guides/adding-authentication-with-auth0.mdx b/website/docs/guides/adding-authentication-with-auth0.mdx index b6ee29c053..cec80c0095 100644 --- a/website/docs/guides/adding-authentication-with-auth0.mdx +++ b/website/docs/guides/adding-authentication-with-auth0.mdx @@ -24,10 +24,15 @@ import { PackageInstall } from '@guild-docs/client' ```ts import { useAuth0 } from '@envelop/auth0' +import { parse, execute, subcribe, validate } from 'graphql' // ... other imports and code const getEnveloped = envelop({ + parse, + execute, + subcribe, + validate, plugins: [ useSchema(schema), useAuth0({ diff --git a/website/docs/guides/integrating-with-databases.md b/website/docs/guides/integrating-with-databases.md index 4d34be690b..908ed8f4c4 100644 --- a/website/docs/guides/integrating-with-databases.md +++ b/website/docs/guides/integrating-with-databases.md @@ -46,6 +46,7 @@ The better way to avoid this is to open only one client per request. With envelo a plugin which adds a client to the context add release it at the end of the request execution. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { isAsyncIterable } from '@envelop/core' import { useSchema } from './use-schema' @@ -73,6 +74,10 @@ const resolvers = { } const getEnvelop = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useSchema(/*...*/), databaseClientPlugin] }) ``` diff --git a/website/docs/guides/migrating-from-v2-to-v3.mdx b/website/docs/guides/migrating-from-v2-to-v3.mdx new file mode 100644 index 0000000000..9e43467453 --- /dev/null +++ b/website/docs/guides/migrating-from-v2-to-v3.mdx @@ -0,0 +1,118 @@ +# Migrating from `v2` to `v3` + +With [new major version](https://github.com/n1ru4l/envelop/pull/1487) comes breaking changes. This section will guide you through the process of migrating from `v2` to `v3`. + +### 1. Remove `graphql` as a peer dependency + +We have designed the new `envelop` to be engine agnostic. This allows you to use any GraphQL engine you want. This means that `graphql` is no longer a peer dependency and `envelop` simply just wraps the `parse`, `validate`, `execute` and `subscribe` functions that you provide. + +```diff +import { envelop } from '@envelop/core'; ++ import { parse, validate, execute, subscribe } from 'graphql'; + +- const getEnveloped = envelop([ ... ]) ++ const getEnveloped = envelop({ parse, validate, execute, subscribe, plugins: [ ... ] }) +``` + +### 2. Removed orchestrator tracing + +We were wrapping the `GraphQLSchema` object but with the new version we no longer want to depend on a specific implementation. + +#### 1. Remove `onResolverCalled` + +We decided to drop this and instead [provide a new plugin](https://github.com/n1ru4l/envelop/pull/1500) that will let you hook into this phase. + +```diff +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop, Plugin } from '@envelop/core' ++ import { useOnResolve } from '@envelop/on-resolve' + +import { onResolverCalled } from './my-resolver' + +function useResolve(): Plugin { + return { +- onResolverCalled: onResolverCalled, ++ onPluginInit: ({ addPlugin }) => { ++ addPlugin(useOnResolve(onResolverCalled)) ++ }, + } +} + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + // ... other plugins ... + useResolve(), + ], +}); +``` + +#### 2. Drop `useTiming` plugin + +This plugin dependended on tracing the schema so we no longer support this out of the box. At this moment we do not have any alternative. We recommned using more advanced tracing solution. If you want to use this feel free to send in a pull request for new plugin! + +#### 3. Remove `EnvelopError` + +To keep the core agnostic from a specific implementation we no longer provide the `EnvelopError` class. To ensure an error is `GraphQLError` envelop check if it an `instanceOf Error` and the name of error is `GraphQLError`. We provide a helper utility `isGraphQLError` to check if an error is a `GraphQLError`. + +### 3. Remove `useAsyncSchema` plugin + +This was a mistake from beginning as we cannot asynchronously `validate` and `parse` since with [`graphql`](https://github.com/graphql/graphql-js) these functions are synchronous in nature. + +You should first load your schema and then create the envelop instance and pass the schema to it. + +```ts +import { envelop, useSchema } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' + +// this assume you are running latest node js version where top-level await is supported +const schema = await loadSchema() + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema)] +}) +``` + +### 4. Rename `useLazyLoadedSchema` to `useSchemaByContext` + +Oringinal name was very misleading since lazy loading could mean it can be asynchronous in nature. This plugin was renamed to better reflect its purpose. It is now called `useSchemaByContext` + +### 5. Remove `enableIf` utility + +This utility was used to enable plugins conditionally. For a better developer experience we have dropped this utility and favor more type safe way to conditionally enable plugins. + +```diff +- import { envelop, useMaskedErrors, enableIf } from '@envelop/core' ++ import { envelop, useMaskedErrors } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' + +const isProd = process.env.NODE_ENV === 'production' + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + // This plugin is enabled only in production +- enableIf(isProd, useMaskedErrors()) ++ isProd && useMaskedErrors() + ] +}) +``` + +### 6. Update options for `useMaskedErrors` plugin + +- We _removed_ `handleValidationErrors` and `handleParseErrors` options since ONLY masking validation errors OR ONLY disabling introspection errors does not make sense, as both can be abused for reverse-engineering the GraphQL schema (see https://github.com/nikitastupin/clairvoyance for reverse-engineering the schema based on validation error suggestions). Instead you should use `useErrorHandler` plugin where you can access each phase and decide what to do with the error. +- Renamed `formatError` to `maskError` + +### 7. Drop support for Node.js v12 + +Node.js v12 is no longer supported by the Node.js team. https://github.com/nodejs/Release/#end-of-life-releases diff --git a/website/docs/guides/monitoring-and-tracing.mdx b/website/docs/guides/monitoring-and-tracing.mdx index b5754b971f..3775ffc014 100644 --- a/website/docs/guides/monitoring-and-tracing.mdx +++ b/website/docs/guides/monitoring-and-tracing.mdx @@ -17,10 +17,15 @@ Sentry is the biggest player regarding error tracking within JavaScript land. Wi As with any other envelop plugin the setup is straight forward! ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useSentry } from '@envelop/sentry' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useSentry() @@ -53,10 +58,15 @@ If you wish to integrate NewRelic for tracing, monitoring and error reporting, y As with any other envelop plugin the setup is straight forward! ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useNewRelic } from '@envelop/newrelic' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useNewRelic({ @@ -73,10 +83,15 @@ const getEnveloped = envelop({ Apollo introduced the apollo-tracing specification and implemented it in apollo-server. With envelop it is possible to use apollo-tracing for tracking down slow resolvers with any server. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloTracing } from '@envelop/apollo-tracing' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloTracing() diff --git a/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx b/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx index 6e05565a05..cf50e67bb9 100644 --- a/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx +++ b/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx @@ -51,11 +51,16 @@ const GraphQLSubscriptionType = new GraphQLObjectType({ As your project scales this, however, can become a tedious task. With the `useContextValuePerExecuteSubscriptionEvent` plugin we abstracted this away by having a generic solution for extending the original context with a new partial before the subscription event is being executed. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useContextValuePerExecuteSubscriptionEvent } from '@envelop/execute-subscription-event' import { createContext, createDataLoaders } from './context' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useContext(() => createContext()), diff --git a/website/docs/guides/securing-your-graphql-api.mdx b/website/docs/guides/securing-your-graphql-api.mdx index e3752f9f11..d81b566f43 100644 --- a/website/docs/guides/securing-your-graphql-api.mdx +++ b/website/docs/guides/securing-your-graphql-api.mdx @@ -74,6 +74,7 @@ Instead of allowing any arbitrary GraphQL operation in production usage, we coul With the [`usePersistedOperations`](/plugins/use-persisted-operations) plugin such an extracted map can easily be used for allow-listing such operations. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { usePersistedOperations, PersistedOperationsStore } from '@envelop/persisted-operations' import persistedOperations from './codegen-artifact' @@ -84,6 +85,10 @@ const store: PersistedOperationsStore = { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ @@ -129,10 +134,15 @@ A handy tool for analyzing your existing GraphQL operations and finding the best You can limit the amount of allowed tokens per operation and automatically abort any further processing of a GraphQL operation document that exceeds the limit with the `maxTokensPlugin`. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { maxTokensPlugin } from '@escape.tech/graphql-armor-max-tokens' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... maxTokensPlugin({ @@ -150,10 +160,15 @@ protection. The [`maxDepthPlugin`](/plugins/graphql-armor-max-depth) allows a maximum nesting level an operation is allowed to have. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { maxDepthPlugin } from '@escape.tech/graphql-armor-max-depth' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... maxDepthPlugin({ @@ -176,10 +191,15 @@ Rate-limiting is a common practice with APIs, and with GraphQL it gets more comp The [`useRateLimiter`](/plugins/use-rate-limiter) to limit access to resources, by a field level. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useRateLimiter } from '@envelop/rate-limiter' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useRateLimiter({ @@ -213,9 +233,15 @@ With the `@envelop/auth0` plugin, you can simply bootstrap the authorization pro ```tsx import { envelop, useExtendContext } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { useAuth0 } from '@envelop/auth0' import { schema } from './schema' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useAuth0({ @@ -263,18 +289,23 @@ Right now envelop ships with two plugins that allow applying authorization befor ### Schema based on context -With the `useLazyLoadedSchema` plugin it is possible to dynamically select a schema for execution based on the context object. This is handy if you have a public schema (e.g. for third-party API consumers) and a private schema (for in-house API consumers). +With the `useSchemaByContext` plugin it is possible to dynamically select a schema for execution based on the context object. This is handy if you have a public schema (e.g. for third-party API consumers) and a private schema (for in-house API consumers). Libraries such as [`graphql-public-schema-filter`](https://github.com/n1ru4l/graphql-public-schema-filter) can be used for generating a schema with only access to a sub part of the original schema using either SDL directives or schema field extensions. ```ts -import { envelop, useLazyLoadedSchema } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop, useSchemaByContext } from '@envelop/core' import { privateSchema, publicSchema } from './schema' const getEnveloped = envelop({ + parse, + validate, + execute, + subscrib, plugins: [ // ... other plugins (e.g. useAuth0) - useLazyLoadedSchema(context => (context.isPrivateApiUser ? privateSchema : publicSchema)) + useSchemaByContext(context => (context.isPrivateApiUser ? privateSchema : publicSchema)) ] }) ``` @@ -286,10 +317,16 @@ With the `useOperationFieldPermissions` plugin you can automatically reject Grap This plugin is perfect for use-cases where you want the whole schema being introspectable, but restrict access to a certain part of the Graph only to specific users. E.g. in a payment subscription model, where API users should only have access to the data that is included within the plan. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop, useSchema } from '@envelop/core' import { useOperationFieldPermissions } from '@envelop/operation-field-permissions' import { schema } from './schema' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins (e.g. useAuth0) useSchema(schema), @@ -338,7 +375,8 @@ In most GraphQL servers any thrown error or rejected promise will result in the ```tsx import { envelop, useSchema, useMaskedErrors, EnvelopError } from '@envelop/core' -import { makeExecutableSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' +import { makeExecutableSchema } from '@graphql-tools/schema' const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` @@ -367,6 +405,10 @@ const schema = makeExecutableSchema({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useSchema(schema), useMaskedErrors()] }) ``` @@ -379,9 +421,14 @@ If your schema includes sensitive information that you want to hide from the out ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { useDisableIntrospection } from '@envelop/disable-introspection' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useDisableIntrospection()] }) ``` @@ -396,9 +443,15 @@ If you disabled schema introspection, you should also disable field suggestions ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' +import { useRateLimiter } from '@envelop/rate-limiter' import { blockFieldSuggestions } from '@escape.tech/graphql-armor-block-field-suggestions' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [blockFieldSuggestions()] }) ``` diff --git a/website/docs/plugins/README.mdx b/website/docs/plugins/README.mdx index 7ddc15b0aa..9ba7443d46 100644 --- a/website/docs/plugins/README.mdx +++ b/website/docs/plugins/README.mdx @@ -22,3 +22,9 @@ Plugins can also change the GraphQL schema during execution - so if your server ## What plugins are available? You can find a list of all plugins, their documentation and installation instructions on the [Envelop Plugin Hub](/plugins). + +## Does the order of plugins matter? + +The plugin order specifies the order in which the handlers of each plugin will be invoked. E.g. the `onExecute` hook of the plugin at the array index 0 will always be invoked before the plugin at the index 1. + +Plugins have the option to stop the execution completly and stop calling further plugins `onExecute` hooks. The `useResponseCache` plugin is such a plugin. Once a response is served from the cache all further `onExecute` are never called and all `onExecuteDone` hooks are never called at all. diff --git a/website/docs/plugins/custom-plugin.mdx b/website/docs/plugins/custom-plugin.mdx index f7f17a9985..58288be6eb 100644 --- a/website/docs/plugins/custom-plugin.mdx +++ b/website/docs/plugins/custom-plugin.mdx @@ -56,6 +56,9 @@ const getEnveloped = envelop({ Often plugins require additional configurartion. A common pattern for doing this is by creating a factor function that returns a `Plugin`. ```ts +import { envelop } from '@envelop/core' +import { parse, validate, subscribe, execute } from 'graphql' + const myPlugin = (shouldPrintResult: boolean): Plugin => { return { onExecute({ args }) { @@ -73,6 +76,10 @@ const myPlugin = (shouldPrintResult: boolean): Plugin => { } const getEnveloped = envelop({ + parse, + validate, + subscribe, + execute, plugins: [ /// ... other plugins ..., myPlugin(true) diff --git a/website/docs/plugins/lifecycle.mdx b/website/docs/plugins/lifecycle.mdx index 37156a43af..1240d4c334 100644 --- a/website/docs/plugins/lifecycle.mdx +++ b/website/docs/plugins/lifecycle.mdx @@ -3,6 +3,8 @@ title: Plugin Lifecycle sidebar_label: Plugin Lifecycle --- +import { Callout } from '@theguild/components' + ## Plugins Lifecycle Plugins are executed in order of their usage, and inject functionality serially, so aim to keep your plugins simple and standalone as much as possible. @@ -36,8 +38,13 @@ In most cases, you'll pass the incoming HTTP request (or, just the relevant part ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ /* ... plugins ... */ ] @@ -239,13 +246,20 @@ Some plugins (like gateway implementations) could potentially change the schema - `schema` - the `GraphQLSchema` - `replaceSchema` - replaces the schema. Calling this will trigger `onSchemaChange` for all other plugins (except for the one that initiated the change); -### `onResolverCalled(api)` +### `onPerform(api)` -Triggered when a resolver is called during the execution of the operation. +Called every time the `perform` function is invoked. Used for augmenting GraphQL parameters and/or setting an early result. -- `params` - an object with `{ root, args, context, info }` that was originally passed to the resolver. +**`before` API**: -You can also return a function to run after the resolver is done, with the following API: +- `context` - the context object built so far by other plugins. +- `extendContext` - extends the context object with additional fields. +- `params` - GraphQL parameters passed to `perform`. +- `setParams` - replace GraphQL parameters before performing. +- `setResult` - sets an early result for immediate response. -- `result` - the resolver return value. -- `setResult` - replaces the resolver return value. +**`onPerformDone` API**: + +- `context` - the context object built so far by other plugins. +- `result` - the execution result, or AsyncIterable in case of stream response. +- `setResult` - replaces the result. diff --git a/website/docs/plugins/testing.mdx b/website/docs/plugins/testing.mdx index 07a2d4056a..b06a7c6992 100644 --- a/website/docs/plugins/testing.mdx +++ b/website/docs/plugins/testing.mdx @@ -9,7 +9,7 @@ The Envelop testkit can also help you to test your envelop plugins in a headless To get stated with the Envelop testkit, make sure to install it in your project (as a `devDependency`): - yarn add -D @envelop/testing + To get started with your `envelop` testing, make sure first to setup your favorite test runner in your project (we use [Jest](https://jestjs.io/)). diff --git a/website/next.config.mjs b/website/next.config.mjs index 39adc44ed9..8d0c3d7fe3 100644 --- a/website/next.config.mjs +++ b/website/next.config.mjs @@ -51,5 +51,20 @@ export default withGuildDocs({ destination: '/plugins/graphql-armor-max-depth', permanent: true, }, + { + source: '/plugins/use-async-schema', + destination: '/docs/guides/migrating-from-v2-to-v3#3-remove-useasyncschema-plugin', + permanent: true, + }, + { + source: '/plugins/use-timing', + destination: '/docs/guides/migrating-from-v2-to-v3#2-drop-usetiming-plugin', + permanent: true, + }, + { + source: '/plugins/use-lazy-loaded-schema', + destination: '/docs/guides/migrating-from-v2-to-v3#4-rename-uselazyloadedschema-to-useschemabycontext', + permanent: true, + }, ], }); diff --git a/website/routes.ts b/website/routes.ts index 8c0c7add3e..31a82ebe68 100644 --- a/website/routes.ts +++ b/website/routes.ts @@ -25,6 +25,7 @@ export function getRoutes(): IRoutes { 'docs/guides': { $name: 'Guides', $routes: [ + ['migrating-from-v2-to-v3', 'Migrating from `v2` to `v3`'], ['securing-your-graphql-api', 'Securing Your GraphQL API'], ['adding-authentication-with-auth0', 'Authentication with Auth0'], ['monitoring-and-tracing', 'Monitoring and Tracing'], diff --git a/website/src/lib/plugins.ts b/website/src/lib/plugins.ts index 917379f51d..de1d2ae141 100644 --- a/website/src/lib/plugins.ts +++ b/website/src/lib/plugins.ts @@ -47,22 +47,11 @@ export const pluginsArr: Package[] = [ tags: ['core', 'schema'], }, { - identifier: 'use-async-schema', - title: 'useAsyncSchema', + identifier: 'use-schema-by-context', + title: 'useSchemaByContext', githubReadme: { repo: 'n1ru4l/envelop', - path: 'packages/core/docs/use-async-schema.md', - }, - npmPackage: '@envelop/core', - iconUrl: '/logo.png', - tags: ['core', 'schema'], - }, - { - identifier: 'use-lazy-loaded-schema', - title: 'useLazyLoadedSchema', - githubReadme: { - repo: 'n1ru4l/envelop', - path: 'packages/core/docs/use-lazy-loaded-schema.md', + path: 'packages/core/docs/use-schema-by-context.md', }, npmPackage: '@envelop/core', iconUrl: '/logo.png', @@ -134,17 +123,6 @@ export const pluginsArr: Package[] = [ iconUrl: '/logo.png', tags: ['core', 'utilities'], }, - { - identifier: 'use-timing', - title: 'useTiming', - githubReadme: { - repo: 'n1ru4l/envelop', - path: 'packages/core/docs/use-timing.md', - }, - npmPackage: '@envelop/core', - iconUrl: '/logo.png', - tags: ['core', 'tracing', 'utilities'], - }, { identifier: 'use-graphql-jit', title: 'useGraphQLJit', diff --git a/yarn.lock b/yarn.lock index a4203ac142..cfd1585d5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4174,14 +4174,14 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== -"@typescript-eslint/eslint-plugin@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz#23d82a4f21aaafd8f69dbab7e716323bb6695cc8" - integrity sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ== - dependencies: - "@typescript-eslint/scope-manager" "5.27.0" - "@typescript-eslint/type-utils" "5.27.0" - "@typescript-eslint/utils" "5.27.0" +"@typescript-eslint/eslint-plugin@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz#6df092a20e0f9ec748b27f293a12cb39d0c1fe4d" + integrity sha512-OwwR8LRwSnI98tdc2z7mJYgY60gf7I9ZfGjN5EjCwwns9bdTuQfAXcsjSB2wSQ/TVNYSGKf4kzVXbNGaZvwiXw== + dependencies: + "@typescript-eslint/scope-manager" "5.36.2" + "@typescript-eslint/type-utils" "5.36.2" + "@typescript-eslint/utils" "5.36.2" debug "^4.3.4" functional-red-black-tree "^1.0.1" ignore "^5.2.0" @@ -4189,69 +4189,70 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.0.tgz#62bb091ed5cf9c7e126e80021bb563dcf36b6b12" - integrity sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA== +"@typescript-eslint/parser@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.36.2.tgz#3ddf323d3ac85a25295a55fcb9c7a49ab4680ddd" + integrity sha512-qS/Kb0yzy8sR0idFspI9Z6+t7mqk/oRjnAYfewG+VN73opAUvmYL3oPIMmgOX6CnQS6gmVIXGshlb5RY/R22pA== dependencies: - "@typescript-eslint/scope-manager" "5.27.0" - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/typescript-estree" "5.27.0" + "@typescript-eslint/scope-manager" "5.36.2" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/typescript-estree" "5.36.2" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz#a272178f613050ed62f51f69aae1e19e870a8bbb" - integrity sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g== +"@typescript-eslint/scope-manager@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.36.2.tgz#a75eb588a3879ae659514780831370642505d1cd" + integrity sha512-cNNP51L8SkIFSfce8B1NSUBTJTu2Ts4nWeWbFrdaqjmn9yKrAaJUBHkyTZc0cL06OFHpb+JZq5AUHROS398Orw== dependencies: - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/visitor-keys" "5.27.0" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/visitor-keys" "5.36.2" -"@typescript-eslint/type-utils@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz#36fd95f6747412251d79c795b586ba766cf0974b" - integrity sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g== +"@typescript-eslint/type-utils@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.36.2.tgz#752373f4babf05e993adf2cd543a763632826391" + integrity sha512-rPQtS5rfijUWLouhy6UmyNquKDPhQjKsaKH0WnY6hl/07lasj8gPaH2UD8xWkePn6SC+jW2i9c2DZVDnL+Dokw== dependencies: - "@typescript-eslint/utils" "5.27.0" + "@typescript-eslint/typescript-estree" "5.36.2" + "@typescript-eslint/utils" "5.36.2" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.0.tgz#c3f44b9dda6177a9554f94a74745ca495ba9c001" - integrity sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A== +"@typescript-eslint/types@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.2.tgz#a5066e500ebcfcee36694186ccc57b955c05faf9" + integrity sha512-9OJSvvwuF1L5eS2EQgFUbECb99F0mwq501w0H0EkYULkhFa19Qq7WFbycdw1PexAc929asupbZcgjVIe6OK/XQ== -"@typescript-eslint/typescript-estree@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz#7965f5b553c634c5354a47dcce0b40b94611e995" - integrity sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ== +"@typescript-eslint/typescript-estree@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.2.tgz#0c93418b36c53ba0bc34c61fe9405c4d1d8fe560" + integrity sha512-8fyH+RfbKc0mTspfuEjlfqA4YywcwQK2Amcf6TDOwaRLg7Vwdu4bZzyvBZp4bjt1RRjQ5MDnOZahxMrt2l5v9w== dependencies: - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/visitor-keys" "5.27.0" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/visitor-keys" "5.36.2" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.0.tgz#d0021cbf686467a6a9499bd0589e19665f9f7e71" - integrity sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA== +"@typescript-eslint/utils@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.36.2.tgz#b01a76f0ab244404c7aefc340c5015d5ce6da74c" + integrity sha512-uNcopWonEITX96v9pefk9DC1bWMdkweeSsewJ6GeC7L6j2t0SJywisgkr9wUTtXk90fi2Eljj90HSHm3OGdGRg== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.27.0" - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/typescript-estree" "5.27.0" + "@typescript-eslint/scope-manager" "5.36.2" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/typescript-estree" "5.36.2" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz#97aa9a5d2f3df8215e6d3b77f9d214a24db269bd" - integrity sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA== +"@typescript-eslint/visitor-keys@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.2.tgz#2f8f78da0a3bad3320d2ac24965791ac39dace5a" + integrity sha512-BtRvSR6dEdrNt7Net2/XDjbYKU5Ml6GqJgVfXT0CxTCJlnIqK7rAGreuWKMT2t8cFUT2Msv5oxw0GMRD7T5J7A== dependencies: - "@typescript-eslint/types" "5.27.0" + "@typescript-eslint/types" "5.36.2" eslint-visitor-keys "^3.3.0" "@tyriar/fibonacci-heap@^2.0.7": @@ -4459,7 +4460,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.2.1: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -6714,7 +6715,7 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== -diff@^4.0.1: +diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== @@ -6731,6 +6732,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +disparity@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/disparity/-/disparity-3.2.0.tgz#7198eaf7a873a130f8098c93061c1df8934500f2" + integrity sha512-8cl9ouncFYE7OQsYwJNiy2e15S0xN80X1Jj/N/YkoiM+VGWSyg1YzPToecKyYx2DQiJapt5IC8yi43GW23TUHQ== + dependencies: + ansi-styles "^4.2.1" + diff "^4.0.2" + dlv@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" @@ -7249,6 +7258,15 @@ eslint-plugin-n@15.2.1: resolve "^1.10.1" semver "^7.3.7" +eslint-plugin-package-json@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-package-json/-/eslint-plugin-package-json-0.1.4.tgz#88ad9ec30f28795c51e9001d4d8294c78889ad7a" + integrity sha512-qdb9LUBFR3tM9OZM1AaYCkxoZnRx7PX2xqvLm49D0JbUu+EkbDur9ug+tCS2xlA1Lbt12Wff5qES81ttc/VBdg== + dependencies: + disparity "^3.0.0" + package-json-validator "^0.6.3" + requireindex "^1.2.0" + eslint-plugin-promise@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz#017652c07c9816413a41e11c30adc42c3d55ff18" @@ -8506,7 +8524,7 @@ graphql-ws@^4.4.2: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.4.2.tgz#f2d83f1863ba3069117199311d664fd28f4aaa8e" integrity sha512-Cz+t1w+8+tiHIKzcz45tMwrxJpPSQ7KNjQrfN8ADAELECkkBB7aSvAgpShWz0Tne8teH/UxzJsULObLVq5eMvQ== -graphql@15.5.1, graphql@16.3.0, graphql@^14.5.3: +graphql@15.5.1, graphql@16.3.0, graphql@16.6.0, graphql@^14.5.3: version "16.3.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.3.0.tgz#a91e24d10babf9e60c706919bb182b53ccdffc05" integrity sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A== @@ -11476,6 +11494,11 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -12051,6 +12074,14 @@ open@^8.2.0: is-docker "^2.1.1" is-wsl "^2.2.0" +optimist@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g== + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -12178,6 +12209,13 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-validator@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/package-json-validator/-/package-json-validator-0.6.3.tgz#aa01a888688b81facf4f83cd7e57e8b6ad1017a1" + integrity sha512-juKiFboV4UKUvWQ+OSxstnyukhuluyuEoFmgZw1Rx21XzmwlgDWLcbl3qzjA3789IRORYhVFs7cmAO0YFGwHCg== + dependencies: + optimist "~0.6.0" + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -13266,6 +13304,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +requireindex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" + integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -15615,6 +15658,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== + worktop@0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/worktop/-/worktop-0.7.0.tgz#d88fd3dcc894715f656d3b80d24433652f455a55"