Skip to content

Commit

Permalink
feat: support errors plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
iamchanii committed Jul 5, 2023
1 parent 5390dea commit 66cde8e
Show file tree
Hide file tree
Showing 21 changed files with 287 additions and 28 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

The Effect integration plugin adds the `t.effect()` field method to implement resolvers using [Effect](https://effect.website/).

## Examples

- [01-basic](/examples/01-basic/)
- [02-with-error-plugin](/examples/02-with-error-plugin/)

## Usage

### Install
Expand Down Expand Up @@ -257,11 +262,6 @@ builder.queryFields(t => ({
}));
```

## Examples

- [01-basic](/examples/01-basic/)
- [02-with-error-plugin](/examples/02-with-error-plugin/)

## Acknowledges

- Pothos by [@hayes](https://github.com/hayes) ([GitHub](https://github.com/hayes/pothos)/[Docs](https://pothos-graphql.dev/)) - A nice GraphQL Schema builder. I heavily relied on the README for this project and The documentation of the plugin implementation is excellent.
Expand Down
13 changes: 0 additions & 13 deletions example/README.md

This file was deleted.

4 changes: 3 additions & 1 deletion examples/01-basic/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
WIP
# Example: Basic

This example demonstrates basic usage of this plugin.
2 changes: 1 addition & 1 deletion example/package.json → examples/01-basic/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "example",
"name": "examples-01-basic",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion example/tsconfig.json → examples/01-basic/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "../tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": false,
"outDir": "./dist"
Expand Down
1 change: 0 additions & 1 deletion examples/02-with-error-plugin/README.md

This file was deleted.

3 changes: 3 additions & 0 deletions examples/02-with-errors-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Example: With Errors Plugin

This example demonstrates how to use [Errors Plugin](https://pothos-graphql.dev/docs/plugins/errors) and get some leverage in catching errors.
17 changes: 17 additions & 0 deletions examples/02-with-errors-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "examples-02-with-errors-plugin",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@pothos/core": "^3.30.0",
"@pothos/plugin-errors": "^3.11.1",
"graphql": "^16.6.0",
"graphql-yoga": "^4.0.1",
"pothos-plugin-effect": "workspace:^"
},
"devDependencies": {
"tsx": "^3.12.7"
}
}
33 changes: 33 additions & 0 deletions examples/02-with-errors-plugin/src/Fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as Context from '@effect/data/Context';
import { pipe } from '@effect/data/Function';
import * as Effect from '@effect/io/Effect';
import * as Layer from '@effect/io/layer';

export class RequestError {
readonly _tag = 'RequestError';
constructor(readonly response: Response | null) {}
}

export interface Fetch {
get(input: RequestInfo | URL, init?: RequestInit | undefined): Effect.Effect<never, RequestError, Response>;
}

export const Fetch = Context.Tag<Fetch>();

export const FetchLive = Layer.succeed(
Fetch,
Fetch.of({
get: (input, init) =>
pipe(
Effect.tryCatchPromise(
() => fetch(input, init),
() => new RequestError(null),
),
Effect.flatMap(response =>
response.ok
? Effect.succeed(response)
: Effect.fail(new RequestError(response))
),
),
}),
);
66 changes: 66 additions & 0 deletions examples/02-with-errors-plugin/src/GitHub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as Context from '@effect/data/Context';
import * as Effect from '@effect/io/Effect';
import * as Layer from '@effect/io/layer';
import { pipe } from 'graphql-yoga';

import { Fetch } from './Fetch.js';

export class NotFound extends Error {
readonly _tag = 'NotFound';

constructor(username: string) {
super(`User ${username} not found`);

this.name = 'NotFound';
}
}

export class ForbiddenUser extends Error {
readonly _tag = 'ForbiddenUser';

constructor(username: string) {
super(`User ${username} is forbidden`);

this.name = 'ForbiddenUser';
}
}

export interface GitHubUser {
followers: number;
name: string;
}

export interface GitHub {
getUser(username: string): Effect.Effect<never, ForbiddenUser | NotFound, GitHubUser>;
}

export const GitHub = Context.Tag<GitHub>();

export const GitHubLive = Layer.effect(
GitHub,
pipe(
Fetch,
Effect.map(fetch =>
GitHub.of({
getUser: username =>
pipe(
Effect.if(
username === 'admin',
/* onTrue */ Effect.fail(new ForbiddenUser(username)),
/* onFalse */ Effect.succeed(username),
),
Effect.flatMap(username => fetch.get(`https://api.github.com/users/${username}`)),
Effect.flatMap(response => Effect.promise(() => response.json() as Promise<GitHubUser>)),
Effect.catchTag('RequestError', () => Effect.fail(new NotFound(username))),
),
})
),
),
);

export const GitHubStub = Layer.succeed(
GitHub,
GitHub.of({
getUser: username => Effect.succeed({ followers: 10_000_000, name: username }),
}),
);
68 changes: 68 additions & 0 deletions examples/02-with-errors-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { pipe } from '@effect/data/Function';
import * as Effect from '@effect/io/Effect';
import SchemaBuilder from '@pothos/core';
import ErrorsPlugin from '@pothos/plugin-errors';
import { createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
import EffectPlugin from 'pothos-plugin-effect';

import { FetchLive } from './Fetch.js';
import { ForbiddenUser, GitHub, GitHubLive, NotFound } from './GitHub.js';

const builder = new SchemaBuilder({
plugins: [ErrorsPlugin, EffectPlugin],
});

const ErrorInterface = builder.interfaceRef<Error>('Error').implement({
fields: (t) => ({
message: t.exposeString('message'),
}),
});

builder.objectType(Error, {
interfaces: [ErrorInterface],
name: 'BaseError',
});

builder.objectType(NotFound, {
interfaces: [ErrorInterface],
name: 'NotFound',
});

builder.objectType(ForbiddenUser, {
interfaces: [ErrorInterface],
name: 'ForbiddenUser',
});

builder.queryType({
fields: t => ({
githubUserFollowers: t.effect({
args: {
username: t.arg.string({ required: true }),
},
effect: {
layers: [FetchLive, GitHubLive],
},
errors: {
types: [NotFound, ForbiddenUser],
},
resolve: (_parent, args) =>
pipe(
GitHub,
Effect.flatMap(github => github.getUser(args.username)),
Effect.map(user => user.followers),
),
type: 'Int',
}),
}),
});

const schema = builder.toSchema({ sortSchema: true });

const yoga = createYoga({ schema });

const server = createServer(yoga);

server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql');
});
8 changes: 8 additions & 0 deletions examples/02-with-errors-plugin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": false,
"outDir": "./dist"
},
"include": ["src"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@effect/data": "^0.12.9",
"@effect/io": "^0.27.1",
"@pothos/core": "^3.30.0",
"@pothos/plugin-errors": "^3.11.1",
"@swc/core": "^1.3.66",
"@swc/jest": "^0.2.26",
"@types/jest": "^29.5.2",
Expand Down
60 changes: 58 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/field-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fieldBuilderProto.effect = function effect({ effect = {}, resolve, ...options })
resolve: (async (_parent: any, _args: any, _context: any, _info: GraphQLResolveInfo) => {
const effectOptions = this.builder.options.effectOptions;

return pipe(
const result = await pipe(
Effect.Do(),
Effect.bind('context', () => {
return pipe(
Expand All @@ -38,9 +38,19 @@ fieldBuilderProto.effect = function effect({ effect = {}, resolve, ...options })
Effect.provideContext(context),
);
}),
Effect.runPromise,
Effect.runPromiseExit,
);

if (result._tag === 'Success') {
return result.value;
}

if (result.cause._tag === 'Annotated' && result.cause.cause._tag === 'Fail') {
throw result.cause.cause.error;
}

throw result.cause;

function getGlobalContextFromBuilderOptions(): Effect.Effect<never, never, Context.Context<any>> {
return pipe(
Effect.gen(function*(_) {
Expand Down
Loading

0 comments on commit 66cde8e

Please sign in to comment.