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

Commit

Permalink
Merge pull request #8 from timkendall/nullable-and-list-types
Browse files Browse the repository at this point in the history
Partially support nullable and list types
  • Loading branch information
timkendall authored Dec 18, 2020
2 parents 8a57db4 + b6b5b80 commit a0f0dd9
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 62 deletions.
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

0 comments on commit a0f0dd9

Please # to comment.