-
Notifications
You must be signed in to change notification settings - Fork 133
Apollo Client - schema link integration #2257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6a7c7d9
025aa5c
6e6830c
7c231c1
47beb2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
|
||
// 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(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <[email protected]>", | ||
"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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { ApolloLink, FetchResult, Observable, Operation } from '@apollo/client'; | ||
import { ComposeContext, GetEnvelopedFn, Optional, Plugin } from '@envelop/types'; | ||
|
||
type ExcludeFalsy<TArray extends any[]> = Exclude<TArray[0], null | undefined | false>[]; | ||
|
||
export namespace EnvelopSchemaLink { | ||
export type ResolverContext = Record<string, any>; | ||
export type ResolverContextFunction = ( | ||
operation: Operation, | ||
) => ResolverContext | PromiseLike<ResolverContext>; | ||
export type Options<PluginsType extends Optional<Plugin<any>>[]> = ReturnType< | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
GetEnvelopedFn<ComposeContext<ExcludeFalsy<PluginsType>>> | ||
>; | ||
} | ||
|
||
/** | ||
* 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<PluginsType extends Optional<Plugin<any>>[]> extends ApolloLink { | ||
private envelope: EnvelopSchemaLink.Options<PluginsType>; | ||
|
||
constructor(options: EnvelopSchemaLink.Options<PluginsType>) { | ||
super(); | ||
this.envelope = options; | ||
} | ||
|
||
public request(operation: Operation): Observable<FetchResult> { | ||
return new Observable<FetchResult>(observer => { | ||
new Promise<EnvelopSchemaLink.ResolverContext>(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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we can also support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be similar to this one; |
||
schema: this.envelope.schema, | ||
document: operation.query, | ||
rootValue: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need this I guess. |
||
execute: this.envelope.execute, | ||
subscribe: this.envelope.subscribe, | ||
Comment on lines
+43
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what to put here. Lee Byron says
I've personally never had the need to use |
||
}, | ||
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); | ||
} | ||
}); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import * as graphqlJs from 'graphql'; | ||
import { ApolloClient, gql, InMemoryCache } from '@apollo/client'; | ||
import { envelop, Plugin, useEngine, useSchema } from '@envelop/core'; | ||
import { makeExecutableSchema } from '@graphql-tools/schema'; | ||
import { EnvelopSchemaLink } from '../src'; | ||
|
||
describe('EnvelopSchemaLink', () => { | ||
let schema = makeExecutableSchema({ | ||
typeDefs: `type Query { hello: String! }`, | ||
resolvers: { | ||
Query: { | ||
hello: () => 'world', | ||
}, | ||
}, | ||
}); | ||
|
||
it('plugin function gets called when querying Apollo', async () => { | ||
let onResult = jest.fn(); | ||
|
||
let testPlugin: Plugin = { | ||
onExecute() { | ||
return { | ||
onExecuteDone({ result }) { | ||
onResult(result); | ||
}, | ||
}; | ||
}, | ||
}; | ||
|
||
let getEnveloped = envelop({ | ||
plugins: [useEngine(graphqlJs), useSchema(schema), testPlugin], | ||
}); | ||
|
||
let envelope = getEnveloped(); | ||
|
||
let apollo = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: new EnvelopSchemaLink(envelope), | ||
}); | ||
|
||
let query = gql` | ||
query { | ||
hello | ||
} | ||
`; | ||
|
||
await apollo.query({ | ||
query, | ||
}); | ||
|
||
let mockResult = onResult.mock.calls[0][0]; | ||
expect(mockResult.data.hello).toEqual('world'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's call it only
EnvelopLink
.EnvelopSchema seems extra :)