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

Partially support nullable and list types #8

Merged
merged 2 commits into from
Dec 18, 2020
Merged
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
122 changes: 97 additions & 25 deletions __tests__/starwars.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ export interface ColorInput {

// "SearchResult" is a union type and not supported

export interface IQuery {
hero: ICharacter;
reviews: IReview[];
search: any;
character: ICharacter;
droid: IDroid;
human: IHuman;
starship: IStarship;
}

export const Query = {
hero: <T extends Array<Field<any, any, any>>>(
variables: { episode?: Variable<"episode"> | Episode },
Expand Down Expand Up @@ -91,6 +101,10 @@ export const Query = {
new Field("starship", [new Argument("id", variables.id)], select(Starship)),
};

export interface IMutation {
createReview: IReview;
}

export const Mutation = {
createReview: <T extends Array<Field<any, any, any>>>(
variables: {
Expand All @@ -109,9 +123,17 @@ export const Mutation = {
),
};

export interface ICharacter {
id: string;
name: string;
friends: ICharacter[];
friendsConnection: IFriendsConnection;
appearsIn: Episode[];
}

export const Character = {
id: () => new Field<"id", [], string>("id"),
name: () => new Field<"name", [], string>("name"),
id: () => new Field<"id">("id"),
name: () => new Field<"name">("name"),

friends: <T extends Array<Field<any, any, any>>>(
select: (t: typeof Character) => T
Expand All @@ -133,16 +155,28 @@ export const Character = {
select(FriendsConnection)
),

appearsIn: () => new Field<"appearsIn", [], Episode>("appearsIn"),
appearsIn: () => new Field<"appearsIn">("appearsIn"),
};

export interface IHuman {
id: string;
name: string;
homePlanet: string;
height: number;
mass: number;
friends: ICharacter[];
friendsConnection: IFriendsConnection;
appearsIn: Episode[];
starships: IStarship[];
}

export const Human = {
id: () => new Field<"id", [], string>("id"),
name: () => new Field<"name", [], string>("name"),
homePlanet: () => new Field<"homePlanet", [], string>("homePlanet"),
id: () => new Field<"id">("id"),
name: () => new Field<"name">("name"),
homePlanet: () => new Field<"homePlanet">("homePlanet"),
height: (variables: { unit: unknown }) =>
new Field<"height", [/* @todo */], number>("height"),
mass: () => new Field<"mass", [], number>("mass"),
new Field<"height", [/* @todo */]>("height"),
mass: () => new Field<"mass">("mass"),

friends: <T extends Array<Field<any, any, any>>>(
select: (t: typeof Character) => T
Expand All @@ -164,16 +198,25 @@ export const Human = {
select(FriendsConnection)
),

appearsIn: () => new Field<"appearsIn", [], Episode>("appearsIn"),
appearsIn: () => new Field<"appearsIn">("appearsIn"),

starships: <T extends Array<Field<any, any, any>>>(
select: (t: typeof Starship) => T
) => new Field("starships", [], select(Starship)),
};

export interface IDroid {
id: string;
name: string;
friends: ICharacter[];
friendsConnection: IFriendsConnection;
appearsIn: Episode[];
primaryFunction: string;
}

export const Droid = {
id: () => new Field<"id", [], string>("id"),
name: () => new Field<"name", [], string>("name"),
id: () => new Field<"id">("id"),
name: () => new Field<"name">("name"),

friends: <T extends Array<Field<any, any, any>>>(
select: (t: typeof Character) => T
Expand All @@ -195,13 +238,19 @@ export const Droid = {
select(FriendsConnection)
),

appearsIn: () => new Field<"appearsIn", [], Episode>("appearsIn"),
primaryFunction: () =>
new Field<"primaryFunction", [], string>("primaryFunction"),
appearsIn: () => new Field<"appearsIn">("appearsIn"),
primaryFunction: () => new Field<"primaryFunction">("primaryFunction"),
};

export interface IFriendsConnection {
totalCount: number;
edges: IFriendsEdge[];
friends: ICharacter[];
pageInfo: IPageInfo;
}

export const FriendsConnection = {
totalCount: () => new Field<"totalCount", [], number>("totalCount"),
totalCount: () => new Field<"totalCount">("totalCount"),

edges: <T extends Array<Field<any, any, any>>>(
select: (t: typeof FriendsEdge) => T
Expand All @@ -216,31 +265,54 @@ export const FriendsConnection = {
) => new Field("pageInfo", [], select(PageInfo)),
};

export interface IFriendsEdge {
cursor: string;
node: ICharacter;
}

export const FriendsEdge = {
cursor: () => new Field<"cursor", [], string>("cursor"),
cursor: () => new Field<"cursor">("cursor"),

node: <T extends Array<Field<any, any, any>>>(
select: (t: typeof Character) => T
) => new Field("node", [], select(Character)),
};

export interface IPageInfo {
startCursor: string;
endCursor: string;
hasNextPage: boolean;
}

export const PageInfo = {
startCursor: () => new Field<"startCursor", [], string>("startCursor"),
endCursor: () => new Field<"endCursor", [], string>("endCursor"),
hasNextPage: () => new Field<"hasNextPage", [], boolean>("hasNextPage"),
startCursor: () => new Field<"startCursor">("startCursor"),
endCursor: () => new Field<"endCursor">("endCursor"),
hasNextPage: () => new Field<"hasNextPage">("hasNextPage"),
};

export interface IReview {
stars: number;
commentary: string;
}

export const Review = {
stars: () => new Field<"stars", [], number>("stars"),
commentary: () => new Field<"commentary", [], string>("commentary"),
stars: () => new Field<"stars">("stars"),
commentary: () => new Field<"commentary">("commentary"),
};

export interface IStarship {
id: string;
name: string;
length: number;
coordinates: number[];
}

export const Starship = {
id: () => new Field<"id", [], string>("id"),
name: () => new Field<"name", [], string>("name"),
id: () => new Field<"id">("id"),
name: () => new Field<"name">("name"),
length: (variables: { unit: unknown }) =>
new Field<"length", [/* @todo */], number>("length"),
coordinates: () => new Field<"coordinates", [], number>("coordinates"),
new Field<"length", [/* @todo */]>("length"),
coordinates: () => new Field<"coordinates">("coordinates"),
};

export const query = <T extends Array<Field<any, any, any>>>(
Expand Down
1 change: 1 addition & 0 deletions __tests__/starwars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe("starwars schema", () => {
// @fix TypeScript currently has issues deriving `Field` types when we have
// two `Selector` objects with identical fields that reference each other.
//
// ex. type ExampleQuery = Result<IQuery, typeof operation.selectionSet.selections>
// t.friends(t => [t.id(), t.name(), t.appearsIn()]),

t.starships((t) => [t.id(), t.name()]),
Expand Down
13 changes: 9 additions & 4 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ export class Client {
// @todo
}

export const execute = <T extends Array<Field<any, any, any, any>>>(
export const execute = <
RootType,
SelectionSet extends Array<Field<any, any, any>>
>(
endpoint: string,
operation: Operation<T>
operation: Operation<SelectionSet>
/* @todo variables?: Variables */
): Promise<ExecutionResult<Result<T>>> =>
): Promise<ExecutionResult<Result<RootType, SelectionSet>>> =>
fetch(endpoint, {
method: "post",
headers: { "Content-Type": "application/json" },
Expand All @@ -20,7 +23,9 @@ export const execute = <T extends Array<Field<any, any, any, any>>>(
// variables,
query: operation.toString(),
}),
}).then((res) => res.json()) as Promise<ExecutionResult<Result<T>>>;
}).then((res) => res.json()) as Promise<
ExecutionResult<Result<RootType, SelectionSet>>
>;

// @todo we lose out type-saftey going through this (need to fix the `Selector` type param)
//
Expand Down
37 changes: 33 additions & 4 deletions src/Codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ const toPrimitive = (
}
};

const renderInterfaceField = (field: GraphQLField<any, any, any>): string => {
const isList =
field.type instanceof GraphQLList ||
(field.type instanceof GraphQLNonNull &&
field.type.ofType instanceof GraphQLList);
const isNonNull = field.type instanceof GraphQLNonNull;
const baseType = getBaseOutputType(field.type);

if (baseType instanceof GraphQLScalarType) {
return `${field.name}: ${toPrimitive(baseType)}` + (isList ? "[]" : "");
} else if (baseType instanceof GraphQLEnumType) {
return `${field.name}: ${baseType.name}` + (isList ? "[]" : "");
} else if (
baseType instanceof GraphQLInterfaceType ||
baseType instanceof GraphQLObjectType
) {
return `${field.name}: I${baseType.name}` + (isList ? "[]" : "");
} else {
return `${field.name}: any`;
}
};

export class Codegen {
private readonly printer = ts.createPrinter();
private readonly source: ts.SourceFile;
Expand Down Expand Up @@ -137,7 +159,12 @@ export class Codegen {
private interfaceType(type: GraphQLInterfaceType): string {
const fields = Object.values(type.getFields());

// @note Render interface types and selector objects
return `
export interface I${type.name} {
${fields.map(renderInterfaceField).join("\n")}
}

export const ${type.name} = {
${fields.map((field) => this.field(field)).join("\n")}
}
Expand All @@ -155,6 +182,10 @@ export class Codegen {
const fields = Object.values(type.getFields());

return `
export interface I${type.name} {
${fields.map(renderInterfaceField).join("\n")}
}

export const ${type.name} = {
${fields.map((field) => this.field(field)).join("\n")}
}
Expand Down Expand Up @@ -211,10 +242,8 @@ export class Codegen {
return args.length > 0
? `${name}: (variables: { ${args
.map((a) => `${a.name}: unknown`)
.join(
", "
)} }) => new Field<"${name}", [/* @todo */], ${fieldType}>("${name}"),`
: `${name}: () => new Field<"${name}", [], ${fieldType}>("${name}"),`;
.join(", ")} }) => new Field<"${name}", [/* @todo */]>("${name}"),`
: `${name}: () => new Field<"${name}">("${name}"),`;
} else {
const renderArgument = (arg: GraphQLArgument): string => {
const _base = getBaseInputType(arg.type);
Expand Down
44 changes: 23 additions & 21 deletions src/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,27 @@ import {
nonNullTypeOf,
} from "./AST";

export type Result<Selection extends Array<Field<any, any, any, any>>> = {
[Key in Selection[number]["name"]]: Selection[number] extends infer U
? U extends Field<Key, any, infer ValueOrSelection, infer Type>
? ValueOrSelection extends Primitive
? Type // derive nullable and array scalars here
: ValueOrSelection extends Array<Field<any, any, any, any>>
? Type extends Array<any>
? Array<Result<ValueOrSelection>> | null
: Result<ValueOrSelection> // Derive nullable and array objects here
: never
: never
: never;
};

export class Operation<T extends Array<Field<any, any, any, any>>> {
export type Result<
Type, // @note Has to be object or array! (right?)
SelectionSet extends Array<Field<any, any, any>>
> = Type extends Array<infer T>
? T extends Primitive
? // @note Return scalar array
Array<T>
: // @note Wrap complex object in array
Array<Result<T, SelectionSet>>
: {
// @note Build out complex object
[Key in SelectionSet[number]["name"]]: Type[Key] extends Primitive
? Type[Key]
: SelectionSet[number] extends infer U
? U extends Field<Key, any, infer Selections>
? Result<Type[Key], Selections>
: never
: never;
};

export class Operation<T extends Array<Field<any, any, any>>> {
constructor(
public readonly name: string,
// @todo support `mutation` and `subscription` operations
Expand Down Expand Up @@ -83,16 +89,12 @@ export class SelectionSet<T extends Array<Field<any, any, any>>> {
export class Field<
Name extends string,
Arguments extends Argument<string, any>[] | never = never,
SelectionSetOrValue extends
| Primitive
| Field<any, any, any>[]
| undefined = undefined,
Type = unknown
SelectionSet extends Field<any, any, any>[] | never = never
> {
constructor(
public readonly name: Name,
public readonly args?: Arguments,
public readonly selectionSet?: SelectionSetOrValue
public readonly selectionSet?: SelectionSet
) {}

get ast(): FieldNode {
Expand Down
Loading