Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/apollo-client/README.md
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.
42 changes: 42 additions & 0 deletions examples/apollo-client/index.ts
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';
Copy link
Member

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 :)

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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think getEnveloped should be called per operation instead of once.
Because the lifecycle in Envelop is handled per operation. So it would be better to call it inside the link.


// 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();
23 changes: 23 additions & 0 deletions examples/apollo-client/package.json
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"
}
}
26 changes: 26 additions & 0 deletions examples/apollo-client/tsconfig.json
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"]
}
52 changes: 52 additions & 0 deletions packages/plugins/apollo-client/README.md
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)
```
74 changes: 74 additions & 0 deletions packages/plugins/apollo-client/package.json
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"
}
}
65 changes: 65 additions & 0 deletions packages/plugins/apollo-client/src/index.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<
Copy link
Author

@vladinator1000 vladinator1000 Jun 24, 2024

Choose a reason for hiding this comment

The 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({
Copy link
Member

@ardatan ardatan Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we can also support subscribe ?
Also keep on mind that execute can return AsyncIterable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schema: this.envelope.schema,
document: operation.query,
rootValue: {
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Author

@vladinator1000 vladinator1000 Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what to put here. Lee Byron says

The root value represents the "top" of your metaphorical graph of data, and is useful to include functions or data to help resolve the root fields in your schema.

I've personally never had the need to use rootValue. It seems like Apollo's SchemaLink has it as an option

},
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);
}
});
});
}
}
54 changes: 54 additions & 0 deletions packages/plugins/apollo-client/test/envelopSchemaLink.spec.ts
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');
});
});
Loading