Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
WIP: Add t.effect on field builder (#40)
Browse files Browse the repository at this point in the history
* Add `t.effect` types

* Add changeset

* Add changeset

* Update

* Changeset
  • Loading branch information
iamchanii authored May 19, 2024
1 parent 2335ee0 commit b6e035a
Show file tree
Hide file tree
Showing 27 changed files with 1,034 additions and 698 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-crabs-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pothos-plugin-effect": minor
---

Add `t.effect` on field builder
7 changes: 7 additions & 0 deletions .changeset/strange-monkeys-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"pothos-plugin-effect": minor
"graphql-effect": minor
"effect-utils": minor
---

Support Stream
2 changes: 1 addition & 1 deletion packages/effect-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"devDependencies": {
"@types/node": "^20.12.7",
"@vitest/coverage-v8": "*",
"effect": "^3.0.6",
"effect": "^3.1.0",
"nanobundle": "*",
"typescript": "*",
"vitest": "*"
Expand Down
1 change: 1 addition & 0 deletions packages/effect-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { executeEffect } from './executeEffect.js';
export { executeStream } from './executeStream.js';
export { isStream } from './isStream.js';
export type { InferValueType } from './types.js';
18 changes: 18 additions & 0 deletions packages/effect-utils/src/isStream.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from 'vitest';
import { isStream } from './isStream.js';
import { Effect, pipe, Stream } from 'effect';

test('should returns false if value is Effect', () => {
const result = isStream(Effect.succeed(1));
expect(result).toBe(false);
});

test('should returns true if value is Stream', () => {
const result = isStream(Stream.make(1));
expect(result).toBe(true);
});

test('should returns false if value is not Effect or Stream ', () => {
const result = isStream({ pipe: () => void 0 });
expect(result).toBe(false);
});
12 changes: 12 additions & 0 deletions packages/effect-utils/src/isStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Effect, Stream } from 'effect';

// This is a unsafe way to check if a value is a Stream.
// I hope that the Effect will expose the `Stream.isStream` function.
export const isStream = <A, E, R>(
value: unknown,
): value is Stream.Stream<A, E, R> => {
return (
Effect.isEffect(value) === false &&
Object.getPrototypeOf(value).constructor.name.startsWith('Stream')
);
};
2 changes: 1 addition & 1 deletion packages/graphql-effect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"devDependencies": {
"@graphql-tools/schema": "^10.0.3",
"@types/node": "^20.12.7",
"effect": "^3.0.6",
"effect": "^3.1.0",
"graphql": "^16.8.1",
"nanobundle": "*",
"typescript": "*",
Expand Down
43 changes: 43 additions & 0 deletions packages/graphql-effect/src/effectResolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Effect, Stream } from 'effect';
import { GraphQLFieldResolver } from 'graphql';
import { expect, test } from 'vitest';
import { effectResolver } from './effectResolver.js';

const fromAsync = async <T>(iterator: AsyncIterable<T>) => {
const result = [];

for await (const value of iterator) {
result.push(value);
}

return result;
};

test('should resolve Effect as a resolver value', async () => {
const resolve: GraphQLFieldResolver<unknown, unknown> = async (
_source,
_args,
_context,
_info,
) => Effect.succeed(1);

const result = await effectResolver(resolve)({}, {}, {}, {} as never);

expect(result).toBe(1);
});

test('should resolve Stream as a resolver value', async () => {
const resolve: GraphQLFieldResolver<unknown, unknown> = async (
_source,
_args,
_context,
_info,
) => Stream.range(1, 5);

const result = await fromAsync(
// @ts-ignore
await effectResolver(resolve)({}, {}, {}, {} as never),
);

expect(result).toEqual([1, 2, 3, 4, 5]);
});
24 changes: 24 additions & 0 deletions packages/graphql-effect/src/effectResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Effect, Runtime } from 'effect';
import { executeEffect, executeStream, isStream } from 'effect-utils';
import type { GraphQLFieldResolver } from 'graphql';

export const effectResolver =
(
resolve: GraphQLFieldResolver<any, any>,
runtime = Runtime.defaultRuntime,
): GraphQLFieldResolver<any, any> =>
async (source, args, context, info) => {
const result = await resolve(source, args, context, info);

if (Effect.isEffect(result)) {
// @ts-ignore
return executeEffect(result, runtime);
}

if (isStream(result)) {
// @ts-ignore
return executeStream(result, runtime);
}

return result;
};
65 changes: 62 additions & 3 deletions packages/graphql-effect/src/enableExecuteEffect.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { makeExecutableSchema } from '@graphql-tools/schema';
import { Context, Effect, Layer, Scope } from 'effect';
import { execute, parse } from 'graphql';
import { Context, Effect, Layer, Scope, Stream } from 'effect';
import { execute, parse, subscribe } from 'graphql';
import { expect, test } from 'vitest';
import { enableExecuteEffect } from './enableExecuteEffect.js';

const fromAsync = async <T>(iterator: AsyncIterable<T>) => {
const result = [];

for await (const value of iterator) {
result.push(value);
}

return result;
};

interface EntityService {
getEntityContent(): Effect.Effect<string>;
}
Expand All @@ -18,7 +28,12 @@ const baseSchema = makeExecutableSchema({
typeDefs: `type Query {
foo: String!
bar: String!
}`,
}
type Subscription {
foo: Int!
}
`,
resolvers: {
Query: {
foo: () => {
Expand All @@ -30,6 +45,12 @@ const baseSchema = makeExecutableSchema({
return yield* entityService.getEntityContent();
}),
},
Subscription: {
foo: {
subscribe: () => Stream.range(1, 5),
resolve: (value) => value,
},
},
},
});

Expand Down Expand Up @@ -63,3 +84,41 @@ test('should return "bar" as result', async () => {
}
`);
});

test('should resolve Stream as subscription', async () => {
const schema = enableExecuteEffect(baseSchema);
const result = await subscribe({
schema,
document: parse(`subscription { foo }`),
});

expect(await fromAsync(result as any)).toMatchInlineSnapshot(`
[
{
"data": {
"foo": 1,
},
},
{
"data": {
"foo": 2,
},
},
{
"data": {
"foo": 3,
},
},
{
"data": {
"foo": 4,
},
},
{
"data": {
"foo": 5,
},
},
]
`);
});
22 changes: 8 additions & 14 deletions packages/graphql-effect/src/enableExecuteEffect.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { MapperKind, mapSchema } from '@graphql-tools/utils';
import { Effect, Runtime } from 'effect';
import { executeEffect } from 'effect-utils';
import { Runtime } from 'effect';
import { GraphQLSchema } from 'graphql';
import { effectResolver } from './effectResolver.js';

export function enableExecuteEffect<R>(
schema: GraphQLSchema,
runtime: Runtime.Runtime<R> = Runtime.defaultRuntime as never,
) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD](fieldConfig) {
const originalResolve = fieldConfig.resolve;
if (fieldConfig.resolve) {
fieldConfig.resolve = effectResolver(fieldConfig.resolve, runtime);
}

fieldConfig.resolve = async (source, args, context, info) => {
if (originalResolve) {
const result = await originalResolve(source, args, context, info);

if (Effect.isEffect(result)) {
return executeEffect(result as never, runtime);
}

return result;
}
};
if (fieldConfig.subscribe) {
fieldConfig.subscribe = effectResolver(fieldConfig.subscribe, runtime);
}

return fieldConfig;
},
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-effect/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { enableExecuteEffect } from './enableExecuteEffect.js';
export { effectResolver } from './effectResolver.js';
5 changes: 2 additions & 3 deletions packages/pothos-plugin-effect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"better-sqlite3": "^9.4.0",
"drizzle-kit": "^0.20.14",
"drizzle-orm": "^0.29.3",
"effect": "^3.0.0",
"effect": "^3.1.5",
"graphql": "*",
"nanobundle": "*",
"prisma": "5.9.1",
Expand All @@ -63,8 +63,7 @@
"graphql": "^16 || ^17"
},
"dependencies": {
"effect-utils": "workspace:^",
"type-plus": "^7.6.0"
"effect-utils": "workspace:^"
},
"packageManager": "[email protected]"
}
Loading

0 comments on commit b6e035a

Please sign in to comment.