From 6a7c7d90fc83858f621d7cd0827e73b6a1652d04 Mon Sep 17 00:00:00 2001 From: Vlady Veselinov Date: Sat, 22 Jun 2024 18:04:22 +0100 Subject: [PATCH 1/5] add apollo schema link support --- packages/plugins/apollo-client/package.json | 74 ++++++++ packages/plugins/apollo-client/src/index.ts | 65 +++++++ .../apollo-client/test/apolloClient.spec.ts | 53 ++++++ pnpm-lock.yaml | 175 +++++++++++++++++- 4 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 packages/plugins/apollo-client/package.json create mode 100644 packages/plugins/apollo-client/src/index.ts create mode 100644 packages/plugins/apollo-client/test/apolloClient.spec.ts diff --git a/packages/plugins/apollo-client/package.json b/packages/plugins/apollo-client/package.json new file mode 100644 index 0000000000..d71a5a50f2 --- /dev/null +++ b/packages/plugins/apollo-client/package.json @@ -0,0 +1,74 @@ +{ + "name": "@envelop/apollo-client", + "version": "0.1.0", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/n1ru4l/envelop.git", + "directory": "packages/plugins/apollo-client" + }, + "author": "Vlady Veselinov ", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "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", + "peerDependencies": { + "@envelop/core": "^5.0.0", + "@envelop/types": "^5.0.0", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "dependencies": { + "@apollo/client": "3.10.4" + }, + "devDependencies": { + "@envelop/core": "workspace:^", + "@graphql-tools/schema": "10.0.4", + "graphql": "16.8.1", + "typescript": "5.1.3" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "sideEffects": false, + "buildOptions": { + "input": "./src/index.ts" + }, + "typescript": { + "definition": "dist/typings/index.d.ts" + } +} diff --git a/packages/plugins/apollo-client/src/index.ts b/packages/plugins/apollo-client/src/index.ts new file mode 100644 index 0000000000..b3141ec6a0 --- /dev/null +++ b/packages/plugins/apollo-client/src/index.ts @@ -0,0 +1,65 @@ +import { ApolloLink, FetchResult, Observable, Operation } from '@apollo/client'; +import { ComposeContext, GetEnvelopedFn, Optional, Plugin } from '@envelop/types'; + +type ExcludeFalsy = Exclude[]; + +export namespace EnvelopSchemaLink { + export type ResolverContext = Record; + export type ResolverContextFunction = ( + operation: Operation, + ) => ResolverContext | PromiseLike; + export type Options>[]> = ReturnType< + GetEnvelopedFn>> + >; +} + +/** + * Lets you use Envelop with Apollo Client. Useful for server-side rendering. + * Inspired by SchemaLink https://github.com/apollographql/apollo-client/blob/main/src/link/schema/index.ts#L8 + */ +export class EnvelopSchemaLink>[]> extends ApolloLink { + private envelope: EnvelopSchemaLink.Options; + + constructor(options: EnvelopSchemaLink.Options) { + super(); + this.envelope = options; + } + + public request(operation: Operation): Observable { + return new Observable(observer => { + new Promise(resolve => + resolve(this.envelope.contextFactory), + ) + .then(context => { + const validationErrors = this.envelope.validate(this.envelope.schema, operation.query); + + if (validationErrors.length > 0) { + return { errors: validationErrors }; + } + + return this.envelope.execute({ + schema: this.envelope.schema, + document: operation.query, + rootValue: { + execute: this.envelope.execute, + subscribe: this.envelope.subscribe, + }, + contextValue: context, + variableValues: operation.variables, + operationName: operation.operationName, + }); + }) + .then(data => { + if (!observer.closed) { + observer.next(data); + observer.complete(); + } + }) + .catch(error => { + if (!observer.closed) { + observer.error(error); + } + }); + }); + } +} diff --git a/packages/plugins/apollo-client/test/apolloClient.spec.ts b/packages/plugins/apollo-client/test/apolloClient.spec.ts new file mode 100644 index 0000000000..4b5dfdb241 --- /dev/null +++ b/packages/plugins/apollo-client/test/apolloClient.spec.ts @@ -0,0 +1,53 @@ +import * as graphqlJs from 'graphql'; +import { ApolloClient, gql, InMemoryCache } from '@apollo/client'; +import { envelop, useEngine, useLogger, useSchema } from '@envelop/core'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { EnvelopSchemaLink } from '../src'; + +describe('apolloClient', () => { + let schema = makeExecutableSchema({ + typeDefs: `type Query { hello: String! }`, + resolvers: { + Query: { + hello: (root, args, context) => 'world', + }, + }, + }); + + it('calls plugin function', async () => { + let logMock = jest.fn(); + + let getEnveloped = envelop({ + plugins: [ + useEngine(graphqlJs), + useSchema(schema), + useLogger({ + logFn(eventName, args) { + logMock(eventName, args); + }, + }), + ], + }); + + let envelope = getEnveloped(); + + let apollo = new ApolloClient({ + cache: new InMemoryCache(), + link: new EnvelopSchemaLink(envelope), + }); + + let query = gql` + query { + hello + } + `; + + await apollo.query({ + query, + }); + + let mockCallResult = logMock.mock.calls[1][1].result; + + expect(mockCallResult.data.hello).toEqual('world'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7442b8ad3..8357f8835e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -653,6 +653,29 @@ importers: version: 5.1.3 publishDirectory: dist + packages/plugins/apollo-client: + dependencies: + '@apollo/client': + specifier: 3.10.4 + version: 3.10.4(@types/react@18.2.69)(graphql@16.6.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@envelop/types': + specifier: ^5.0.0 + version: 5.0.0 + devDependencies: + '@envelop/core': + specifier: workspace:^ + version: link:../../core/dist + '@graphql-tools/schema': + specifier: 10.0.4 + version: 10.0.4(graphql@16.6.0) + graphql: + specifier: 16.6.0 + version: 16.6.0 + typescript: + specifier: 5.1.3 + version: 5.1.3 + publishDirectory: dist + packages/plugins/apollo-datasources: dependencies: tslib: @@ -1617,6 +1640,24 @@ packages: peerDependencies: graphql: 16.6.0 + '@apollo/client@3.10.4': + resolution: {integrity: sha512-51gk0xOwN6Ls1EbTG5svFva1kdm2APHYTzmFhaAdvUQoJFDxfc0UwQgDxGptzH84vkPlo1qunY1FuboyF9LI3Q==} + peerDependencies: + graphql: 16.6.0 + graphql-ws: ^5.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + '@apollo/core-schema@0.2.3': resolution: {integrity: sha512-0MXK/rlo2Es6qp4nb5lkMcN8jz3AaXm7TiPENO9Cyyy8kIC6rTKKpHCd1yv/E5aDEIFFq44LJcL+WrLONSy7+g==} engines: {node: '>=12.13.0 <18.0'} @@ -4669,6 +4710,26 @@ packages: resolution: {integrity: sha512-LS8tSomZa3YHnntpWt3PP43iFEEl6YeIsvDakczHBKlay5LdkXFr8w7v8H6akpG5nRrzydyB0k1iE2eoL6aKIQ==} engines: {node: '>=16.0.0'} + '@wry/caches@1.0.1': + resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==} + engines: {node: '>=8'} + + '@wry/context@0.7.4': + resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==} + engines: {node: '>=8'} + + '@wry/equality@0.5.7': + resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==} + engines: {node: '>=8'} + + '@wry/trie@0.4.3': + resolution: {integrity: sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==} + engines: {node: '>=8'} + + '@wry/trie@0.5.0': + resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==} + engines: {node: '>=8'} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -7211,6 +7272,9 @@ packages: hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -8967,6 +9031,9 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + optimism@0.18.0: + resolution: {integrity: sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==} + optimist@0.6.1: resolution: {integrity: sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==} @@ -9731,6 +9798,17 @@ packages: resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true + rehackt@0.1.0: + resolution: {integrity: sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==} + peerDependencies: + '@types/react': '*' + react: '*' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + rehype-katex@7.0.0: resolution: {integrity: sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==} @@ -9833,6 +9911,10 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + response-iterator@0.2.6: + resolution: {integrity: sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==} + engines: {node: '>=0.8'} + restore-cursor@1.0.1: resolution: {integrity: sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==} engines: {node: '>=0.10.0'} @@ -10485,6 +10567,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + sync-request@6.1.0: resolution: {integrity: sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==} engines: {node: '>=8.0.0'} @@ -10662,6 +10748,10 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-invariant@0.10.3: + resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==} + engines: {node: '>=8'} + ts-is-defined@1.0.0: resolution: {integrity: sha512-HmzqN8xWETXnfpXyUqMf5nvcZszn9aTNjxVIJ6R2aNNg14oLo3PCi9IRhsv+vg2C7TI90M7PyjBOrg4f6/nupA==} @@ -11386,6 +11476,12 @@ packages: resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} engines: {node: '>=10'} + zen-observable-ts@1.2.5: + resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==} + + zen-observable@0.8.15: + resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + zod-validation-error@1.5.0: resolution: {integrity: sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==} engines: {node: '>=16.0.0'} @@ -11416,6 +11512,29 @@ snapshots: dependencies: graphql: 16.6.0 + '@apollo/client@3.10.4(@types/react@18.2.69)(graphql@16.6.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.6.0) + '@wry/caches': 1.0.1 + '@wry/equality': 0.5.7 + '@wry/trie': 0.5.0 + graphql: 16.6.0 + graphql-tag: 2.12.6(graphql@16.6.0) + hoist-non-react-statics: 3.3.2 + optimism: 0.18.0 + prop-types: 15.8.1 + rehackt: 0.1.0(@types/react@18.2.69)(react@18.2.0) + response-iterator: 0.2.6 + symbol-observable: 4.0.0 + ts-invariant: 0.10.3 + tslib: 2.6.2 + zen-observable-ts: 1.2.5 + optionalDependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + '@apollo/core-schema@0.2.3(graphql@16.6.0)': dependencies: graphql: 16.6.0 @@ -15045,9 +15164,9 @@ snapshots: '@types/express@4.17.3': dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.0 - '@types/serve-static': 1.15.7 + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.33 + '@types/serve-static': 1.13.9 '@types/form-data@0.0.33': dependencies: @@ -15440,6 +15559,26 @@ snapshots: fast-querystring: 1.1.2 tslib: 2.6.2 + '@wry/caches@1.0.1': + dependencies: + tslib: 2.6.2 + + '@wry/context@0.7.4': + dependencies: + tslib: 2.6.2 + + '@wry/equality@0.5.7': + dependencies: + tslib: 2.6.2 + + '@wry/trie@0.4.3': + dependencies: + tslib: 2.6.2 + + '@wry/trie@0.5.0': + dependencies: + tslib: 2.6.2 + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -18750,6 +18889,10 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + hosted-git-info@2.8.9: {} hot-shots@9.3.0: @@ -21287,6 +21430,13 @@ snapshots: opener@1.5.2: {} + optimism@0.18.0: + dependencies: + '@wry/caches': 1.0.1 + '@wry/context': 0.7.4 + '@wry/trie': 0.4.3 + tslib: 2.6.2 + optimist@0.6.1: dependencies: minimist: 0.0.8 @@ -22079,6 +22229,11 @@ snapshots: dependencies: jsesc: 0.5.0 + rehackt@0.1.0(@types/react@18.2.69)(react@18.2.0): + optionalDependencies: + '@types/react': 18.2.69 + react: 18.2.0 + rehype-katex@7.0.0: dependencies: '@types/hast': 3.0.1 @@ -22232,6 +22387,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + response-iterator@0.2.6: {} + restore-cursor@1.0.1: dependencies: exit-hook: 1.1.1 @@ -23037,6 +23194,8 @@ snapshots: csso: 5.0.5 picocolors: 1.0.0 + symbol-observable@4.0.0: {} + sync-request@6.1.0: dependencies: http-response-object: 3.0.2 @@ -23268,6 +23427,10 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-invariant@0.10.3: + dependencies: + tslib: 2.6.2 + ts-is-defined@1.0.0: dependencies: ts-tiny-invariant: 0.0.3 @@ -24114,6 +24277,12 @@ snapshots: property-expr: 2.0.4 toposort: 2.0.2 + zen-observable-ts@1.2.5: + dependencies: + zen-observable: 0.8.15 + + zen-observable@0.8.15: {} + zod-validation-error@1.5.0(zod@3.22.4): dependencies: zod: 3.22.4 From 025aa5c41546c796c9be679a09643b4611dbfa0e Mon Sep 17 00:00:00 2001 From: Vlady Veselinov Date: Sat, 22 Jun 2024 18:40:59 +0100 Subject: [PATCH 2/5] update readme --- packages/plugins/apollo-client/README.md | 52 +++++++++++++++++++ ...ient.spec.ts => envelopSchemaLink.spec.ts} | 6 +-- 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 packages/plugins/apollo-client/README.md rename packages/plugins/apollo-client/test/{apolloClient.spec.ts => envelopSchemaLink.spec.ts} (88%) diff --git a/packages/plugins/apollo-client/README.md b/packages/plugins/apollo-client/README.md new file mode 100644 index 0000000000..915cd00a57 --- /dev/null +++ b/packages/plugins/apollo-client/README.md @@ -0,0 +1,52 @@ +## `@envelop/apollo-client` + +Lets you use Envelop with Apollo client via a SchemaLink. Useful when you want to re-use your server +graphql setup, while avoiding network calls during server-side rendering. + +## Getting Started + +``` +yarn add graphql-middleware @envelop/apollo-client +``` + +## Usage Example + +```ts +import * as graphqlJs from 'graphql' +import { ApolloClient, gql, InMemoryCache } from '@apollo/client' +import { EnvelopSchemaLink } from '@envelop/apollo-client' +import { envelop, useEngine, useSchema } from '@envelop/core' +import { makeExecutableSchema } from '@graphql-tools/schema' + +let schema = makeExecutableSchema({ + typeDefs: `type Query { hello: String! }`, + resolvers: { + Query: { + hello: () => 'world' + } + } +}) + +// Use any enveloped setup +let getEnveloped = envelop({ + plugins: [useEngine(graphqlJs), useSchema(schema)] +}) +let envelope = getEnveloped() + +let apollo = new ApolloClient({ + cache: new InMemoryCache(), + // Pass it to EnvelopSchemaLink, this is the key + link: new EnvelopSchemaLink(envelope) +}) + +// Use Apollo +let result = await apollo.query({ + query: gql` + query { + hello + } + ` +}) + +console.log(result) +``` diff --git a/packages/plugins/apollo-client/test/apolloClient.spec.ts b/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts similarity index 88% rename from packages/plugins/apollo-client/test/apolloClient.spec.ts rename to packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts index 4b5dfdb241..f421b21020 100644 --- a/packages/plugins/apollo-client/test/apolloClient.spec.ts +++ b/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts @@ -4,17 +4,17 @@ import { envelop, useEngine, useLogger, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { EnvelopSchemaLink } from '../src'; -describe('apolloClient', () => { +describe('EnvelopSchemaLink', () => { let schema = makeExecutableSchema({ typeDefs: `type Query { hello: String! }`, resolvers: { Query: { - hello: (root, args, context) => 'world', + hello: () => 'world', }, }, }); - it('calls plugin function', async () => { + it('plugin function gets called when querying Apollo', async () => { let logMock = jest.fn(); let getEnveloped = envelop({ From 6e6830cbeac5410fdefa699b07103e8ea685df70 Mon Sep 17 00:00:00 2001 From: Vlady Veselinov Date: Mon, 24 Jun 2024 17:54:09 +0100 Subject: [PATCH 3/5] add apollo client example --- examples/apollo-client/README.md | 10 ++++++ examples/apollo-client/index.ts | 42 +++++++++++++++++++++++++ examples/apollo-client/package.json | 23 ++++++++++++++ examples/apollo-client/tsconfig.json | 26 +++++++++++++++ website/src/pages/docs/integrations.mdx | 1 + 5 files changed, 102 insertions(+) create mode 100644 examples/apollo-client/README.md create mode 100644 examples/apollo-client/index.ts create mode 100644 examples/apollo-client/package.json create mode 100644 examples/apollo-client/tsconfig.json diff --git a/examples/apollo-client/README.md b/examples/apollo-client/README.md new file mode 100644 index 0000000000..dd963c4f1c --- /dev/null +++ b/examples/apollo-client/README.md @@ -0,0 +1,10 @@ +## Envelop example with [`Apollo client`](https://github.com/apollographql/apollo-client). + +Useful for server-side rendering that avoids network calls, if you're rendering on the same server +that the GraphQL API runs on. + +## Running this example + +1. Install all dependencies from the root of the repo (using `pnpm i`) +2. `cd` into the example and run `pnpm start`. +3. Open http://localhost:3000 in your browser, look for logs in the console. diff --git a/examples/apollo-client/index.ts b/examples/apollo-client/index.ts new file mode 100644 index 0000000000..af5fa8f696 --- /dev/null +++ b/examples/apollo-client/index.ts @@ -0,0 +1,42 @@ +import * as graphqlJs from 'graphql'; +import { ApolloClient, gql, InMemoryCache } from '@apollo/client'; +import { EnvelopSchemaLink } from '@envelop/apollo-client'; +import { envelop, useEngine, useSchema } from '@envelop/core'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +const schema = makeExecutableSchema({ + typeDefs: `type Query { hello: String! }`, + resolvers: { + Query: { + hello: () => 'world', + }, + }, +}); + +// Use any enveloped setup +const getEnveloped = envelop({ + plugins: [useEngine(graphqlJs), useSchema(schema)], +}); +const envelope = getEnveloped(); + +// Pass envelope to EnvelopSchemaLink +const apollo = new ApolloClient({ + cache: new InMemoryCache(), + link: new EnvelopSchemaLink(envelope), +}); + +async function runExampleQuery() { + // Use Apollo in your app + const result = await apollo.query({ + query: gql` + query { + hello + } + `, + }); + + // eslint-disable-next-line no-console + console.log(result); +} + +runExampleQuery(); diff --git a/examples/apollo-client/package.json b/examples/apollo-client/package.json new file mode 100644 index 0000000000..9e71525bb9 --- /dev/null +++ b/examples/apollo-client/package.json @@ -0,0 +1,23 @@ +{ + "name": "@envelop-examples/apollo-client", + "version": "1.0.0", + "author": "Vlady Veselinov", + "license": "MIT", + "private": true, + "main": "index.js", + "scripts": { + "start": "ts-node index.ts" + }, + "dependencies": { + "@apollo/client": "^3.10.6", + "@envelop/apollo-client": "workspace:^", + "@envelop/core": "workspace:^", + "@graphql-tools/schema": "10.0.4", + "graphql": "16.9.0" + }, + "devDependencies": { + "@types/node": "20.11.30", + "ts-node": "10.9.2", + "typescript": "5.1.3" + } +} diff --git a/examples/apollo-client/tsconfig.json b/examples/apollo-client/tsconfig.json new file mode 100644 index 0000000000..fb7db3b867 --- /dev/null +++ b/examples/apollo-client/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "target": "es2020", + "lib": ["es2020"], + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "outDir": "dist", + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "noImplicitAny": false, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "importHelpers": true, + "skipLibCheck": true + }, + "include": ["."], + "exclude": ["node_modules"] +} diff --git a/website/src/pages/docs/integrations.mdx b/website/src/pages/docs/integrations.mdx index e2a2d8d71e..67e9fb3b9b 100644 --- a/website/src/pages/docs/integrations.mdx +++ b/website/src/pages/docs/integrations.mdx @@ -30,6 +30,7 @@ missing your favorite server, feel free to add it! | Node.js HTTP | ✅ | [`basic-http`](https://github.com/n1ru4l/envelop/tree/main/examples/simple-http) | | GraphQL-Helix (with `@defer` and `@stream`) | ✅ | [`graphql-helix`](https://github.com/n1ru4l/envelop/tree/main/examples/graphql-helix-defer-stream) | | Apollo-Server | [Partial](https://github.com/apollographql/apollo-server/discussions/5541) | [`apollo-server`](https://github.com/n1ru4l/envelop/tree/main/examples/apollo-server) | +| Apollo Client | ✅ | [`apollo-client`](https://github.com/n1ru4l/envelop/tree/main/examples/apollo-client) | | GraphQL-HTTP | ✅ | [`graphql-http`](https://github.com/n1ru4l/envelop/tree/main/examples/graphql-http) | | GraphQL-WS | ✅ | [`graphql-ws`](https://github.com/n1ru4l/envelop/tree/main/examples/graphql-ws) | | GraphQL-SSE | ✅ | [`graphql-sse`](https://github.com/n1ru4l/envelop/tree/main/examples/graphql-sse) | From 7c231c1806fe5572934768e4eca655312846e6f5 Mon Sep 17 00:00:00 2001 From: Vlady Veselinov Date: Mon, 24 Jun 2024 18:16:26 +0100 Subject: [PATCH 4/5] simplify EnvelopSchemaLink test --- .../test/envelopSchemaLink.spec.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts b/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts index f421b21020..312c038e16 100644 --- a/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts +++ b/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts @@ -1,6 +1,6 @@ import * as graphqlJs from 'graphql'; import { ApolloClient, gql, InMemoryCache } from '@apollo/client'; -import { envelop, useEngine, useLogger, useSchema } from '@envelop/core'; +import { envelop, Plugin, useEngine, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { EnvelopSchemaLink } from '../src'; @@ -17,16 +17,18 @@ describe('EnvelopSchemaLink', () => { it('plugin function gets called when querying Apollo', async () => { let logMock = jest.fn(); - let getEnveloped = envelop({ - plugins: [ - useEngine(graphqlJs), - useSchema(schema), - useLogger({ - logFn(eventName, args) { - logMock(eventName, args); + let testPlugin: Plugin = { + onExecute() { + return { + onExecuteDone({ result }) { + logMock(result); }, - }), - ], + }; + }, + }; + + let getEnveloped = envelop({ + plugins: [useEngine(graphqlJs), useSchema(schema), testPlugin], }); let envelope = getEnveloped(); @@ -46,8 +48,7 @@ describe('EnvelopSchemaLink', () => { query, }); - let mockCallResult = logMock.mock.calls[1][1].result; - + let mockCallResult = logMock.mock.calls[0][0]; expect(mockCallResult.data.hello).toEqual('world'); }); }); From 47beb2e572e94951274df23041c2d3b077ef69e9 Mon Sep 17 00:00:00 2001 From: Vlady Veselinov Date: Mon, 24 Jun 2024 18:18:09 +0100 Subject: [PATCH 5/5] improve SchemaLink test naming --- .../plugins/apollo-client/test/envelopSchemaLink.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts b/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts index 312c038e16..008456e8dd 100644 --- a/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts +++ b/packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts @@ -15,13 +15,13 @@ describe('EnvelopSchemaLink', () => { }); it('plugin function gets called when querying Apollo', async () => { - let logMock = jest.fn(); + let onResult = jest.fn(); let testPlugin: Plugin = { onExecute() { return { onExecuteDone({ result }) { - logMock(result); + onResult(result); }, }; }, @@ -48,7 +48,7 @@ describe('EnvelopSchemaLink', () => { query, }); - let mockCallResult = logMock.mock.calls[0][0]; - expect(mockCallResult.data.hello).toEqual('world'); + let mockResult = onResult.mock.calls[0][0]; + expect(mockResult.data.hello).toEqual('world'); }); });