Skip to content

Commit

Permalink
move more reusable logic into toolkit package (#2401)
Browse files Browse the repository at this point in the history
* deprecate unused property

* move async helper functions to toolkit

* move format functions to toolkit

* add changeset

* more elaborate comment
  • Loading branch information
thomasheyenbrock authored May 13, 2022
1 parent cb39d02 commit 60a744b
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 136 deletions.
6 changes: 6 additions & 0 deletions .changeset/rude-singers-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'graphiql': patch
'@graphiql/toolkit': patch
---

move async helper functions and formatting functions over into the @graphiql/toolkit package
5 changes: 5 additions & 0 deletions .changeset/wise-maps-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/toolkit': patch
---

deprecate the unused `shouldPersistHeaders` property from fetcher options to be removed in the next major version
89 changes: 89 additions & 0 deletions packages/graphiql-toolkit/src/async-helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
FetcherResult,
FetcherReturnType,
Observable,
} from '../create-fetcher';

// Duck-type promise detection.
export function isPromise<T>(value: Promise<T> | any): value is Promise<T> {
return typeof value === 'object' && typeof value.then === 'function';
}

// Duck-type Observable.take(1).toPromise()
function observableToPromise<T>(observable: Observable<T>): Promise<T> {
return new Promise((resolve, reject) => {
const subscription = observable.subscribe({
next: v => {
resolve(v);
subscription.unsubscribe();
},
error: reject,
complete: () => {
reject(new Error('no value resolved'));
},
});
});
}

// Duck-type observable detection.
export function isObservable<T>(value: any): value is Observable<T> {
return (
typeof value === 'object' &&
'subscribe' in value &&
typeof value.subscribe === 'function'
);
}

export function isAsyncIterable(
input: unknown,
): input is AsyncIterable<unknown> {
return (
typeof input === 'object' &&
input !== null &&
// Some browsers still don't have Symbol.asyncIterator implemented (iOS Safari)
// That means every custom AsyncIterable must be built using a AsyncGeneratorFunction (async function * () {})
((input as any)[Symbol.toStringTag] === 'AsyncGenerator' ||
Symbol.asyncIterator in input)
);
}

function asyncIterableToPromise<T>(
input: AsyncIterable<T> | AsyncIterableIterator<T>,
): Promise<T> {
return new Promise((resolve, reject) => {
// Also support AsyncGenerator on Safari iOS.
// As mentioned in the isAsyncIterable function there is no Symbol.asyncIterator available
// so every AsyncIterable must be implemented using AsyncGenerator.
const iteratorReturn = ('return' in input
? input
: input[Symbol.asyncIterator]()
).return?.bind(input);
const iteratorNext = ('next' in input
? input
: input[Symbol.asyncIterator]()
).next.bind(input);

iteratorNext()
.then(result => {
resolve(result.value);
// ensure cleanup
iteratorReturn?.();
})
.catch(err => {
reject(err);
});
});
}

export function fetcherReturnToPromise(
fetcherResult: FetcherReturnType,
): Promise<FetcherResult> {
return Promise.resolve(fetcherResult).then(result => {
if (isAsyncIterable(result)) {
return asyncIterableToPromise(result);
} else if (isObservable(result)) {
return observableToPromise(result);
}
return result;
});
}
5 changes: 5 additions & 0 deletions packages/graphiql-toolkit/src/create-fetcher/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export type FetcherParams = {

export type FetcherOpts = {
headers?: { [key: string]: any };
/**
* @deprecated This property will be removed in the next major version of
* `graphiql`, it just echoes back the value passed as prop to the `GraphiQL`
* component with a default value of `false`
*/
shouldPersistHeaders?: boolean;
documentAST?: DocumentNode;
};
Expand Down
48 changes: 48 additions & 0 deletions packages/graphiql-toolkit/src/format/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { GraphQLError, GraphQLFormattedError } from 'graphql';

function stringify(obj: unknown): string {
return JSON.stringify(obj, null, 2);
}

const formatSingleError = (error: Error): Error => ({
...error,
// Raise these details even if they're non-enumerable
message: error.message,
stack: error.stack,
});

type InputError = Error | GraphQLError | string;

function handleSingleError(
error: InputError,
): GraphQLFormattedError | Error | string {
if (error instanceof GraphQLError) {
return error.toString();
}
if (error instanceof Error) {
return formatSingleError(error);
}
return error;
}

type GenericError =
| Error
| readonly Error[]
| string
| readonly string[]
| GraphQLError
| readonly GraphQLError[];

export function formatError(error: GenericError): string {
if (Array.isArray(error)) {
return stringify({
errors: error.map((e: InputError) => handleSingleError(e)),
});
}
// @ts-ignore
return stringify({ errors: handleSingleError(error) });
}

export function formatResult(result: any): string {
return stringify(result);
}
2 changes: 2 additions & 0 deletions packages/graphiql-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './async-helpers';
export * from './create-fetcher';
export * from './format';
// TODO: move the most useful utilities from graphiql to here
Loading

0 comments on commit 60a744b

Please sign in to comment.