Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execution result type includes the full schema #12

Open
Pajn opened this issue Mar 28, 2019 · 15 comments
Open

Execution result type includes the full schema #12

Pajn opened this issue Mar 28, 2019 · 15 comments
Labels
enhancement New feature or request

Comments

@Pajn
Copy link

Pajn commented Mar 28, 2019

It would be very nice if the result type only included the selected fields.
I imagine this could be solvable by using a combination of mapped types and conditional types.

Have you looked into this at and have some experience on the possibility?

@mikecann
Copy link

Agreed, this is very almost perfect, except for that point... ill have a quick play and see if I can crack it..

@helios1138
Copy link
Owner

helios1138 commented Mar 29, 2019

@Pajn I can maybe see how mapped types can be used to pick the first level properties based on the query, but I can't see a solution on how to make it work with an arbitrarily nested GraphQL query/response.

Possibly there is a way to have at least some N-levels-down solution...

@helios1138
Copy link
Owner

Okay, looked at conditional types. This shows more promise

@mikecann
Copy link

mikecann commented Mar 29, 2019 via email

@helios1138
Copy link
Owner

helios1138 commented Mar 29, 2019

@mikecann this is what I have so far. Appears to be working recursively.

interface User {
  id: string
  name: string
  email: string | null
  posts: Post[]
}

interface Post {
  id: string
  title: string
  description: string | null
  author: User
}

interface UserRequest {
  id?: boolean | number
  name?: boolean | number
  email?: boolean | number
  posts?: PostRequest
}

interface PostRequest {
  id?: boolean | number
  title?: boolean | number
  description?: boolean | number
  author?: UserRequest
}

type UserPartial<R extends UserRequest> =
  (R extends { id: boolean | number } ? { id: string } : {}) &
  (R extends { name: boolean | number } ? { name: string } : {}) &
  (R extends { email: boolean | number } ? { email: string | null } : {}) &
  (R extends { posts: PostRequest } ? { posts: PostPartial<R['posts']>[] } : {})

type PostPartial<R extends PostRequest> =
  (R extends { id: boolean | number } ? { id: string } : {}) &
  (R extends { title: boolean | number } ? { title: string } : {}) &
  (R extends { description: boolean | number } ? { description: string | null } : {}) &
  (R extends { author: UserRequest } ? { author: UserPartial<R['author']> } : {})

const query = <T>(r: T extends UserRequest ? UserRequest : T): UserPartial<T> => {
  throw new Error('')
}

@mikecann
Copy link

Oh lol, you beat me too it, this is what I just came up with:

image

// The main grapQL type
type List = {
  name: string;
  age: number;
  something: {
    foo: "bar"
  }
};

type Req<T> = { [P in keyof T]?: number | boolean } & {
  __typename?: boolean | number;
  __scalar?: boolean | number;
};

function makeExecute<U>() {
  return function execute<T extends Req<U> >(
    request: T & Req<U>,
    defaultValue?: U
  ): { [P in keyof T]: P extends keyof U ? U[P] : never } {
    throw "put implementation here";
  }
}

const execute = makeExecute<List>();
const x = execute({ name: 1, something: 1  });

@mikecann
Copy link

Ill see if I can make mine recursive with your types..

@mikecann
Copy link

image

interface User {
  id: string
  name: string
  email: string | null
  posts: Post[]
}

interface Post {
  id: string
  title: string
  description: string | null
  author: User
}

type RequestProp<T> = T extends string ?  number | boolean :  Req<T>;

type Req<T> = { [P in keyof T]?: RequestProp<T[P]> } & {
  __typename?: boolean | number;
  __scalar?: boolean | number;
};

function makeExecute<U>() {
  return function execute<T extends Req<U> >(
    request: T & Req<U>,
    defaultValue?: U
  ): { [P in keyof T]: P extends keyof U ? U[P] : never } {
    throw "put implementation here";
  }
}

const x = makeExecute<Post>()({ title: 1,  author: { id: 1 } });

const y = makeExecute<User>()({  });

gotta go to bed now, but its almost there, just needs to handle arrays as part of that conditional.

@mikecann
Copy link

Oh man I must have been tired, because the above doesnt work for recursive types..

@mikecann
Copy link

image

Here this solves it recursively, it makes your brain bleed a little trying to work it out tho..

type Scalar = string | number | boolean;

interface User {
  id: string;
  name: string;
  age: number;
  email: string | null;
  posts: Post[];
}

interface Post {
  id: string;
  title: string;
  description: string | null;
  author: User;
}

type Selectable = number | boolean;

type RequestSelctionField<TFieldValue> = TFieldValue extends Scalar
  ? Selectable
  : TFieldValue extends (infer X)[]
  ? RequestSelection<X>
  : RequestSelection<TFieldValue>;

type RequestSelection<TObject> = {
  [P in keyof TObject]?: RequestSelctionField<TObject[P]>
} & {
  __typename?: Selectable;
  __scalar?: Selectable;
};

type ReturnProp<
  TObject,
  TRequestSelection,
  TField extends keyof TObject,
  TObjectValue = TObject[TField]
> = TObjectValue extends Scalar
  ? TObjectValue
  : TField extends keyof TRequestSelection
  ? TObjectValue extends (infer X)[]
    ? Returned<X, TRequestSelection[TField]>[]
    : Returned<TObject[TField], TRequestSelection[TField]>
  : never;

type Returned<TObject, TRequestSelection> = {
  [TField in keyof TRequestSelection]: TField extends keyof TObject
    ? ReturnProp<TObject, TRequestSelection, TField>
    : never
};

function makeExecute<TObject>() {
  return function execute<TRequest extends RequestSelection<TObject>>(
    request: TRequest & RequestSelection<TObject>,
    defaultValue?: TObject
  ): Returned<TObject, TRequest> {
    throw "put implementation here";
  };
}

const x = makeExecute<Post>()({
  title: 1,
  author: { id: 1, age: 1, posts: { id: 1 } }
});

const xx = x.author.posts[0].

@mikecann
Copy link

mikecann commented Mar 31, 2019

BTW using the above one could make a very nice fluent syntax using just a bit more typing and proxy objects.

image

type QueryArgsMap = {
  markTagSuggestions: QueryMarkTagSuggestionsArgs;
  marks: QueryMarksArgs;
  mark: QueryMarkArgs;
};

type QueryArgs<P, T> = P extends keyof QueryArgsMap ? QueryArgsMap[P] : never;

type Executable<TObject> = {
  execute: <TRequest extends RequestSelection<TObject>>(
    request: TRequest & RequestSelection<TObject>
  ) => Returned<TObject, TRequest>;
};

type MakeQueryable<T> = {
  [P in keyof T]: (variables: QueryArgs<P, T>) => Executable<T[P]>
};

type Root = {
  query: MakeQueryable<Query>;
};

let root: Root = {} as any;

const resp = root.query.marks({ input: { query: "foo" } }).execute({ totalCount: 1, marks: { id: 1 } });
resp.marks[0].

The above is using the schema from one of my own projects but will work with any object type.

... thinking about it more one could just export GraphQL AST much like https://github.com/apollographql/graphql-tag does, then it could be consumed by Apollo Client and you wouldnt have to write your own client with caching and all other other features that Apollo supports..

maybe https://github.com/prisma/nexus already does most of this..

@helios1138
Copy link
Owner

helios1138 commented Apr 3, 2019

Here this solves it recursively, it makes your brain bleed a little trying to work it out tho..

do you think your solution can fit the __scalar logic and return types that are unions/interfaces, not specific objects? the solution I'm currently considering relies on pre-generating the conditional types like these

export type UserPartial<
  R extends UserRequest,
  F1 extends UserRequest = _FR<R['friends'], UserRequest>,
  F2 extends PostRequest = _FR<R['posts'], PostRequest>,
  F3 extends PetRequest = _FR<R['pets'], PetRequest>
> = (R extends Required<Pick<R, 'id'>> ? { id: ID } : {}) &
  (R extends Required<Pick<R, 'username'>> ? { username: String } : {}) &
  (R extends Required<Pick<R, 'email'>> ? { email: String } : {}) &
  (R extends Required<Pick<R, 'wasEmployed'>> ? { wasEmployed: Boolean | null } : {}) &
  (R extends Required<Pick<R, 'friends'>> ? { friends: UserPartial<F1>[] | null } : {}) &
  (R extends Required<Pick<R, 'posts'>> ? { posts: PostPartial<F2>[] } : {}) &
  (R extends Required<Pick<R, 'pets'>> ? { pets: PetPartial<F3>[] } : {})

It also does not yet implements __scalar or unions/interfaces, but at least I can see how it can potentially be implemented, since all the information is there during the generation process. not sure how can that information can be extracted in your solution, if it only relies on inference. Maybe only pre-generate the lists of scalar props. Curious.

BTW using the above one could make a very nice fluent syntax using just a bit more typing and proxy objects.

not exactly sure what you have there. is the goal to get the same API as it is now, but with less pre-generated ts code?

you wouldnt have to write your own client with caching and all other other features that Apollo supports..

Currently, you can also use Apollo client here easily, inside the fetching mechanism

@mikecann
Copy link

mikecann commented Apr 4, 2019

@helios1138 Awesome. I think you should go with your technique if you can get it to work.

I spent half a day trying to come up with a clever non / minimal codegen solution when I should have been working on my project and although theoretically I could make it work I just dont have the time for it right now.

BTW I even spent a coupple of hours on a pair programming session with a TS evangelist who works closely with the compiler team on this problem :) Its a really fun problem to solve, wish I had a bit more time on it.

For now tho, go with your solution if it works. Im keen to use it in my project!

@helios1138 helios1138 added the enhancement New feature or request label Apr 5, 2019
@jgoux
Copy link

jgoux commented Feb 18, 2020

Hello,

I'm very interested by this client and I think this part is the last missing piece. 😍
I very much agree with @mikecann on this

... thinking about it more one could just export GraphQL AST much like https://github.com/apollographql/graphql-tag does, then it could be consumed by Apollo Client and you wouldnt have to write your own client with caching and all other other features that Apollo supports..

I'd like a fully typed equivalent of graphql-tag to pass to any client (in my case I'd use react-query).
It's like in CSS in JS, they all went from the css taged template to a typed object alternative, and I feel that we're very close to it here.

How can I help?

@remorses
Copy link

remorses commented May 3, 2020

Graphql Zeus uses a generic type MapType to select some fields DST from an interface SRC

type MapType<SRC extends Anify<DST>, DST> = ...

View the code here

We can simply copy paste that code and have the feature right now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants