0no-co / GraphQLSP

TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.
https://gql-tada.0no.co
MIT License
362 stars 14 forks source link

Change return type based on query #131

Closed jasonkuhrt closed 5 months ago

jasonkuhrt commented 9 months ago

Hey, this LSP looks promising, thanks for creating it!

I was playing around with this:

CleanShot 2023-12-08 at 12 13 00@2x

And noticed there is no inference. Since this is an LSP I believe it should be possible to dynamically augment the return type based on the argument (query) given?

Right now, what would have to happen is, an import of the typed document generated, which is:

import * as Types from "../../__generated__/baseGraphQLSP"
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type FooQueryVariables = Types.Exact<{ [key: string]: never; }>;

export type FooQuery = { __typename: 'Query', workspace: { __typename: 'Workspace', id: string } };

export const FooDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"foo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"StringValue","value":"abc","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<FooQuery, FooQueryVariables>;

The FoodDocument is, IIUC, a duplication of what the gql template literal will produce.

There may be times when the current approach fits with established systems but it should not be the only way I think. Using this tool should make the DX on par with genql.

By having inference, it would also remove the need to name the queries, further simplifying things. Code would become simple like:

CleanShot 2023-12-08 at 12 22 36@2x

kitten commented 9 months ago

And noticed there is no inference. Since this is an LSP I believe it should be possible to dynamically augment the return type based on the argument (query) given?

This isn't possible in TypeScript. The type checking system is isolated and opaque to LSP plugins, even if the plugin system was interlinked with the compiler more closely, that'd still at most allow for build output transformations. And even still, if it was interlinked with the type checker, there isn't a prepared entry point to alter types and step into type checking phases.

I also don't think that this will change any time soon, or that it's reasonable for TS to change this necessarily

There may be times when the current approach fits with established systems but it should not be the only way I think. Using this tool should make the DX on par with genql.

Personally, and this may differ from other people's views, I find modelling DSLs into JS methods/objects/classes/etc horrendous.

It works out for CSS, and even then only to an extent, since it's dealing with declarations, i.e. properties and values. Even then there's conflicting views around how to deal with comma separated values vs repeated declarations.

Similar to this, GraphQL has a distinct query language that's purpose-built for its specific properties and that cannot reasonably be mapped to a JS/TS equivalent without making concessions and worsening the developer experience bit by bit with every language feature that's supported and mapped over (directives in general, optionals like defer vs include/skip directives, arguments, aliases, etc all contribute to more obscure inputs having to be written)

There is a solution to this, and it's not exactly novel, but putting in the research into making it work reliably, performant, and comprehensively is taking some time. But personally, imho, the answer will be this once it's done: https://github.com/0no-co/gql.tada

This is a project to infer GraphQL string literals directly to output types via parsing the queries in the type system from an input introspection type. But it'll ultimately take a while to finish and stabilise due to performance considerations ✌️

JoviDeCroock commented 9 months ago

Exactly as Phil said, we aren't able to intercept the internal type nor can we alter it hence settling on the codegen + file transformation, which is actually also quite discouraged/unsupported 😅 the first approach that was tried for this project was intercepting the type but the LSP-plugins are honestly quite limited in terms of what it can reach into.

I share the sentiment that the object-based syntax is limiting, you either complicate argument-variables, directives or fragments in some shape or form.

I would argue that the typed-document-node is quite settled on in terms of a GraphQL standard way to type document-nodes in TypeScript. If you don't want that, the GraphQL-code-generator client-preset is also well supported at the moment!

In the future we can indeed improve this with gql.tada where we would output an Introspection from the LSP so you can enjoy automatic typing everywhere 😅

jasonkuhrt commented 9 months ago

Thanks for the sharing the thoughts. I understand better now the limitations of the LSP. The next idea that comes to mind is generated global types that can be matched to a callsite via a globally unique query name.

const { foo } = await query('abc', `
  query {
    foo {
      bar
    }
  }
`)

This would require coordination between a few bits, but generally should be straight forward. The hard work is already achieved by this tool.

Said coordination would be:

In the above example query would be a user land function that combines the template tag and graphql client that accepts a typed document node.

While simple, It would bring this tool toward aspects it does not currently cover. While it does generate runtime code, it is following an established idiom in the JS community. My proposal is not an established approach.


Regarding https://github.com/0no-co/gql.tada I'm not sure I follow. A usage example would help me.


Regarding merits of JS vs GraphQL syntax, there is a lot to discuss there. I think you're too dismissive and dogmatic, personally. How much do people give up when using Prisma instead of SQL? A lot. When they need SQL can they drop to it? Yes. There are tradeoffs, using GraphQL query syntax, instead of JS. Genql is missing features that are about its limitations, not JS. It could support streaming, defer, aliases, etc. but the author of that tool hasn't done so yet. Despite that, the straight forward toolchain, type checking of inputs and inference of outputs, is a productivity boost that outweighs the benefits of GraphQL syntax, due to the tooling issues. I did not come and make this issue because I wasn't interested in trying GraphQL syntax, after all. But without inference, it does not make sense to use in my current project, and I suspect other ones out there too.

JoviDeCroock commented 9 months ago

What you mention about the coordination comes very close to what graphql-code-generator already does in their client-preset apart from the being indexed by name and being a global it already comes very close.

In your vision this would be generated by the LSP? Personally in my first line of thinking I didn't want to break the whole code-generation idiom as a lot of GraphQL users are subscribed to that so hence we support both tagged-template literals (we generate code and alias the type) like this or the client-preset approach where the generator is external and we facilitate the diagnostics/... like here.

Am I right in understanding that given your proposal these two approaches would get combined and we would generate the graphql() helper as part of the LSP process?

Regarding https://github.com/0no-co/gql.tada I'm not sure I follow. A usage example would help me.

This would be very similar to your approach where the requirements would be an introspection and a document, when combined with TypedDocument it would infer the types based on traversing the introspection with the document at hand.

Example can be found in the linked test-suite, ofcourse this is missing a public API like graphql().

I think you're too dismissive and dogmatic, personally.

You are very much free to think this, I have personally tried a lot of these tools like GraphQL Zeus and others, which may have been too early in their journey but I in fact did run into the limitations I mentioned quite often 😅 not saying that folks can't create a paradigm that fixes all of them but the GraphQL execution language is quite large. I don't want to open this discussion, I did want to "defend" myself from this accusation.

jasonkuhrt commented 9 months ago

Hey @JoviDeCroock I think I have homework to do at this point in regards to the things you mentioned. Thanks for the response! What I am mostly trying to figure out next is what do the existing primitives/building blocks permit, and what is the necessary application level glue code. I'm still a bit hazy on that.

The ideal is to be able to write a GraphQL query and have inference without needing to think about anything else, such as deps or configuration. Editor integration, like an LSP, is uniquely positioned to deliver on this ideal since it can generate any necessary code for the user. From a developer point of view, writing a GraphQL query becomes as native as writing JSX feels nowadays (e.g. when using .tsx).

This degree of seamlessness and no-learning-curve is where GraphQL codegenerator and other tools in the ecosystem currently seem to fail.

Given that GenQL is, for almost no effort, solving 99% of my current use-case(s), I can't prioritize looking more into this proposal at present, but it would be generally great to return to, learn more, and share more thoughts at some point. Feel free to close though if this issue is creating noise, of course.

kitten commented 9 months ago

The ideal is to be able to write a GraphQL query and have inference without needing to think about anything else,

This is basically a goal we're very aligned on. I wrote some extensive notes to answer your questions, but, to be honest, I'll stash and discard them for now, for the sake of being clear and concise ✌️

Basically, there's more to GraphQLSP than it seems, and it's neither finished or arrived at a major milestone past being usable today, nor is it the only part of the solution.

Today, GraphQLSP targets two use-cases:

Apart from these, it obviously looks at the schema and provides auto-suggestions, diagnostics, and hints.

The missing piece here, as you said, are queries. And that's bounded, in our view by the main problem: the GraphQL ecosystem’s tools are increasingly fragmented and their sheer amount if confusing to people. In a typical GraphQL project, people already have to choose up to 5–8 tools, including things like GraphQL Code Generator (amongst clients, query builders/authoring tool, linting, codegen, schema builder, API gateway/server) And basically; the query authoring experience should, in most cases be, basically zero-config.

The goal here is to combine GraphQLSP with gql.tada. The latter is an inference-based GraphQL builder, similar to GraphQL Code Generator but running mostly in the TypeScript type system.

It'll basically take this: (The /* GraphQL */ comment is only needed for editors to add syntax highlighting)

gql(/* GraphQL */`
  { test }
`)

And type the return type automatically via TypeScript inference. GraphQLSP at that point will only provide:

i.e. the goal here is to eliminate tools and processes for users to learn entirely.

That's also where our apprehension to non-DSL query authoring comes from. Tools like GraphQL Zeus or genql as posted are great initial experiences, but supporting all of these tools (there are tens of alternatives; as there usually are for many ideas in the GraphQL ecosystem) and we can't support them all for their fractional adoption (not throwing shade, I literally mean GCG or tagged template strings without types are vastly more popular, and these tools are further splitting adoption up into smaller segments) So, i.e. we want a “native” GraphQL query language experience with minimal to no mental overhead and no tool should need configuring. (For context, even if GCG is used independently with the client-preset — or other plugins — it still needs configuring for the query output types to be as one would expect)

So, that's basically where we're at; GraphQLSP is a stepping stone and supports the (de-facto) biggest tool that adds typegen to GraphQL query documents, and from there we can hopefully streamline this into one recommended setup.

Hope that makes sense ✌️

nandorojo commented 9 months ago

@jasonkuhrt just echoing what the maintainers mentioned – client-preset from GraphQL Codegen is great and does exactly what you want. You can enable the codegen with a watchscript and get instant types as you save.

client-preset has a really nice type system for fragment masking. It's very powerful once you get used to it. I recommend this article.

And it'll work great with GraphQLSP (be sure to check the client-preset setup in its docs).

nandorojo commented 9 months ago

It'll basically take this: (The / GraphQL / comment is only needed for editors to add syntax highlighting)

I actually have syntax highlighting working without even needing that comment when I use the graphql() function from client-preset.

kitten commented 5 months ago

Closing since this isn't relevant with gql.tada being a thing.