Skip to content
This repository has been archived by the owner on Jul 20, 2023. It is now read-only.

Commit

Permalink
Unwrap results in client methods
Browse files Browse the repository at this point in the history
  • Loading branch information
timkendall committed Jun 24, 2021
1 parent c1b3bd4 commit 2f14b43
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 117 deletions.
15 changes: 15 additions & 0 deletions __tests__/starwars/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Executor } from "../../src";
import { Episode, Starwars } from "./starwars.sdk";

describe("Starwars SDK Client", () => {
const starwars = new Starwars(new Executor("http://localhost:8080"));

it.skip("returns type-safe results", async () => {
const reviews = await starwars.query.reviews(
{ episode: Episode.EMPIRE },
(t) => [t.stars(), t.commentary()]
);

expect(reviews?.map((t) => t)).toBeInstanceOf(Array);
});
});
287 changes: 199 additions & 88 deletions __tests__/starwars/starwars.sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Executor,
Client,
TypeConditionError,
ExecutionError,
} from "../../src";

export const VERSION = "unversioned";
Expand Down Expand Up @@ -957,126 +958,236 @@ export class Starwars implements Client {
constructor(public readonly executor: Executor) {}

public readonly query = {
hero: <T extends Array<Selection>>(
hero: async <T extends Array<Selection>>(
variables: { episode?: Episode },
select: (t: CharacterSelector) => T
) =>
this.executor.execute<
IQuery,
Operation<SelectionSet<[Field<"hero", any, SelectionSet<T>>]>>
>(
new Operation(
"hero",
"query",
new SelectionSet([Query.hero<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IQuery,
Operation<SelectionSet<[Field<"hero", any, SelectionSet<T>>]>>
>(
new Operation(
"hero",
"query",
new SelectionSet([Query.hero<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({ name: "hero", transportError: error });
});

if (result.errors) {
throw new ExecutionError({ name: "hero", result });
} else if (result.data) {
return result.data.hero;
} else {
throw new ExecutionError({ name: "hero", result });
}
},

reviews: <T extends Array<Selection>>(
reviews: async <T extends Array<Selection>>(
variables: { episode?: Episode },
select: (t: ReviewSelector) => T
) =>
this.executor.execute<
IQuery,
Operation<SelectionSet<[Field<"reviews", any, SelectionSet<T>>]>>
>(
new Operation(
"reviews",
"query",
new SelectionSet([Query.reviews<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IQuery,
Operation<SelectionSet<[Field<"reviews", any, SelectionSet<T>>]>>
>(
new Operation(
"reviews",
"query",
new SelectionSet([Query.reviews<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({ name: "reviews", transportError: error });
});

if (result.errors) {
throw new ExecutionError({ name: "reviews", result });
} else if (result.data) {
return result.data.reviews;
} else {
throw new ExecutionError({ name: "reviews", result });
}
},

search: <T extends Array<Selection>>(
search: async <T extends Array<Selection>>(
variables: { text?: string },
select: (t: SearchResultSelector) => T
) =>
this.executor.execute<
IQuery,
Operation<SelectionSet<[Field<"search", any, SelectionSet<T>>]>>
>(
new Operation(
"search",
"query",
new SelectionSet([Query.search<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IQuery,
Operation<SelectionSet<[Field<"search", any, SelectionSet<T>>]>>
>(
new Operation(
"search",
"query",
new SelectionSet([Query.search<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({ name: "search", transportError: error });
});

if (result.errors) {
throw new ExecutionError({ name: "search", result });
} else if (result.data) {
return result.data.search;
} else {
throw new ExecutionError({ name: "search", result });
}
},

character: <T extends Array<Selection>>(
character: async <T extends Array<Selection>>(
variables: { id?: string },
select: (t: CharacterSelector) => T
) =>
this.executor.execute<
IQuery,
Operation<SelectionSet<[Field<"character", any, SelectionSet<T>>]>>
>(
new Operation(
"character",
"query",
new SelectionSet([Query.character<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IQuery,
Operation<SelectionSet<[Field<"character", any, SelectionSet<T>>]>>
>(
new Operation(
"character",
"query",
new SelectionSet([Query.character<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({
name: "character",
transportError: error,
});
});

if (result.errors) {
throw new ExecutionError({ name: "character", result });
} else if (result.data) {
return result.data.character;
} else {
throw new ExecutionError({ name: "character", result });
}
},

droid: <T extends Array<Selection>>(
droid: async <T extends Array<Selection>>(
variables: { id?: string },
select: (t: DroidSelector) => T
) =>
this.executor.execute<
IQuery,
Operation<SelectionSet<[Field<"droid", any, SelectionSet<T>>]>>
>(
new Operation(
"droid",
"query",
new SelectionSet([Query.droid<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IQuery,
Operation<SelectionSet<[Field<"droid", any, SelectionSet<T>>]>>
>(
new Operation(
"droid",
"query",
new SelectionSet([Query.droid<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({ name: "droid", transportError: error });
});

if (result.errors) {
throw new ExecutionError({ name: "droid", result });
} else if (result.data) {
return result.data.droid;
} else {
throw new ExecutionError({ name: "droid", result });
}
},

human: <T extends Array<Selection>>(
human: async <T extends Array<Selection>>(
variables: { id?: string },
select: (t: HumanSelector) => T
) =>
this.executor.execute<
IQuery,
Operation<SelectionSet<[Field<"human", any, SelectionSet<T>>]>>
>(
new Operation(
"human",
"query",
new SelectionSet([Query.human<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IQuery,
Operation<SelectionSet<[Field<"human", any, SelectionSet<T>>]>>
>(
new Operation(
"human",
"query",
new SelectionSet([Query.human<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({ name: "human", transportError: error });
});

if (result.errors) {
throw new ExecutionError({ name: "human", result });
} else if (result.data) {
return result.data.human;
} else {
throw new ExecutionError({ name: "human", result });
}
},

starship: <T extends Array<Selection>>(
starship: async <T extends Array<Selection>>(
variables: { id?: string },
select: (t: StarshipSelector) => T
) =>
this.executor.execute<
IQuery,
Operation<SelectionSet<[Field<"starship", any, SelectionSet<T>>]>>
>(
new Operation(
"starship",
"query",
new SelectionSet([Query.starship<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IQuery,
Operation<SelectionSet<[Field<"starship", any, SelectionSet<T>>]>>
>(
new Operation(
"starship",
"query",
new SelectionSet([Query.starship<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({ name: "starship", transportError: error });
});

if (result.errors) {
throw new ExecutionError({ name: "starship", result });
} else if (result.data) {
return result.data.starship;
} else {
throw new ExecutionError({ name: "starship", result });
}
},
};

public readonly mutate = {
createReview: <T extends Array<Selection>>(
createReview: async <T extends Array<Selection>>(
variables: { episode?: Episode; review?: ReviewInput },
select: (t: ReviewSelector) => T
) =>
this.executor.execute<
IMutation,
Operation<SelectionSet<[Field<"createReview", any, SelectionSet<T>>]>>
>(
new Operation(
"createReview",
"mutation",
new SelectionSet([Mutation.createReview<T>(variables, select)])
) => {
const result = await this.executor
.execute<
IMutation,
Operation<SelectionSet<[Field<"createReview", any, SelectionSet<T>>]>>
>(
new Operation(
"createReview",
"mutation",
new SelectionSet([Mutation.createReview<T>(variables, select)])
)
)
),
.catch((error: any) => {
throw new ExecutionError({
name: "createReview",
transportError: error,
});
});

if (result.errors) {
throw new ExecutionError({ name: "createReview", result });
} else if (result.data) {
return result.data.createReview;
} else {
throw new ExecutionError({ name: "createReview", result });
}
},
};
}
35 changes: 27 additions & 8 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ export class Executor {
}
}

// @bug TypeScript 4.2+ error when implementing this interface with HTTPExecutor
// https://github.com/microsoft/TypeScript/issues/34933
// export interface Executor {
// execute<RootType, TOperation extends Operation<SelectionSet<any>>>(
// operation: TOperation
// ): Promise<ExecutionResult<Result<RootType, TOperation["selectionSet"]>>>;
// }

export const httpExecute = <
RootType,
TOperation extends Operation<SelectionSet<any>>
Expand All @@ -46,3 +38,30 @@ export const httpExecute = <
}).then((res) => res.json()) as Promise<
ExecutionResult<Result<RootType, TOperation["selectionSet"]>>
>;

export class ExecutionError extends Error {
public readonly name: string;
public readonly result?: ExecutionResult;
public readonly transportError?: Error;

constructor({
name,
result,
transportError,
}: {
readonly name: string;
readonly result?: ExecutionResult;
readonly transportError?: Error;
}) {
super(
`Failed to execute operation on "${name}". See "ExecutionError.what" for more details.`
);

this.result = result;
this.transportError = transportError;
}

get what() {
return this.transportError ?? this.result;
}
}
Loading

0 comments on commit 2f14b43

Please sign in to comment.