microsoft / cppgraphqlgen

C++ GraphQL schema service generator
MIT License
323 stars 45 forks source link

Plans for cppgraphqlgen 5.0 #311

Open wravery opened 1 week ago

wravery commented 1 week ago

This is where I'm going to start tracking features for 5.0, the next major version of cppgraphqlgen. This will be a breaking change for existing consumers, so it's an opportunity to cleanup deprecated methods and simplify some of the APIs. I'm also going to adopt a few more C++20 and C++23 features if they are well enough supported on Windows, Linux, and macOS now.

This is using #172, the tracking issue for cppgraphqlgen 4.0, as a template and starting point.

I'll update this list as new features crop up and as I/we make progress on it. If there's something you'd like to see that I haven't included already, feel free to comment on this issue.

wravery commented 1 day ago

I'm planning on making a release candidate as soon as the CI build finishes. I haven't done any of the documentation updates yet, but if anyone wants to take it for a spin, 5.0.0 should be completely implemented in the next branch now.

nqf commented 1 day ago

Sorry,I am not qualified to demand anything, but let me share my personal feelings, We compared cpp ,Golang and Rust, ,their(rust,Golang ) grphql framework makes it easier for a novice to start from scratch. Would you consider generating something similar, This is an example of Rust, which really makes it very easy for a beginner to get started, rust-graphql-example

The CPP framework requires us to write some checks ourselves, which is very long and difficult for beginners. In fact, many times we don't need to know the underlying details, we just want to write business logic
cppgraphqlgen-beast-http-server

wravery commented 1 day ago

There are some of those samples out there for cppgraphqlgen, in particular the "Star Wars" learning sample is implemented here: next/main. There are also some unrelated samples linked in the root README.

To be honest, I prefer working in Rust with async-graphql myself. In a situation where I'm just trying to implement a GraphQL service with the performance of a compiled native language, I'd reach for that first. What cppgraphqlgen is aimed at is more of a bridge to existing C++ projects. Similarly, I'd probably pick something else beside cppgraphqlgen or async-graphql if I were trying to connect logic in another language/stack.

The Boost.Beast HTTP server example serves a couple of purposes:

  1. It keeps everything in this project in C++, no need for additional language toolchains.
  2. It demonstrates using C++ coroutines from different libraries, including some of the rough edges.

I don't recommend using the Boost.Beast sample as a starting point for a new web service, though, unless you need to use 100% C++ in your project for some reason. Given my own preference for Rust, even if I needed to expose a cppgraphqlgen service through HTTP, I'd probably use a Rust web server and thinner GraphQL bindings to the cppgraphqlgen service, like in gqlmapi-rs.

nqf commented 1 day ago

Thank you for your reply, Actually, our project is 100% cpp, so I would prefer cpp to have a graphql framework similar to rust‘s async-graphql, But in reality, there is no decent GraphQL framework in the CPP , Therefore, we ultimately chose to use Golang to implement the GraphQL server, But personally, But I sincerely hope that CPP also has a decent(simple + easy-to-use) GraphQL framework

wravery commented 1 day ago

Yeah, unfortunately C++ just can't do it as simply. I haven't used Golang or its GraphQL frameworks, but I am familiar with a couple of Rust GraphQL frameworks, and a lot of their ease-of-use comes from procedural macros. There are a few proposals out there for static reflection in C++ or meta-classes that could do something similar, but AFAIK those are many years away from becoming part of the standard.

I think static code generators are the closest we can get to hiding the ugly details from C++.

nqf commented 1 day ago

Yeah, unfortunately C++ just can't do it as simply. I haven't used Golang or its GraphQL frameworks, but I am familiar with a couple of Rust GraphQL frameworks, and a lot of their ease-of-use comes from procedural macros. There are a few proposals out there for static reflection in C++ or meta-classes that could do something similar, but AFAIK those are many years away from becoming part of the standard.

I think static code generators are the closest we can get to hiding the ugly details from C++.

In fact, cpp can generate code like Golang and finally expose something similar to a callback function. We only need to implement business logic in the callback function

nqf commented 1 day ago

Actually, I think cppgraphqlgen should be able to be packaged into something like this, But it may be necessary to write a code generation tool to generate the graphql_server class

graphql_server.setAcallback([]() ->asio::await{
co_return;
});
graphql_server.setBcallback([]() ->asio::await {
co_return;
});
graphql_server.handler("/graphql", "POST");

graphql_server.start("0.0.0.0:80");
gitmodimo commented 1 day ago

I have been using cppgraphqlgen in production code for about 2 years. Here are some of my thouths:

  1. cppgraphqlgen lacks extensibility. Concept similar to Apollo federation. I think it seems viable to allow joining two graphql::service::Request services as follows: Main schema
    
    schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
    }

type Query type Mutation type Subscription

Extension schema

extend type Query{ feature1: SomeFeature! } type SomeFeature{ testValue: Int! }

I my opinion it should be possible to generate two separate compilation units/libraries that could be literally added to each other at runtime. As in:

std::shared_ptr mainSchema=...; std::shared_ptr feature1=...; if(feature1active) mainSchema+=feature1;


Addition should expand `TypeMap _operations` and merge introspection infofmation.

2. The library is unnecessarily slow because of few reasons:

A. Response is held in `graphql::response::Value` which is DOM representation that _ususally_ converted to json using additional library. This could be shortcut with service taking visitor instead of returning response. In fact default visitor could be  `graphql::response::Value` builder to keep backward compatibility, but it could be replaced by JsonBuilder based on for example RapidJSON.

B. There should be more fine grained control for subscriptions (especially NotifySubscribe and NotifyUnsubscribe). I think subscription should return a handle to allow external fine grained control. 
Ex. In subscription that uses list field it may happen that list changes between NotifySubscribe and NotifyUnsubscribe. In such case NotifyUnsubscribe is useless in "cleaning up" Subscription event sources. I end up using external accounting of tracking the sources and removing then when subscription completes. I want to be able to skip NotifyUnsubscribe step and just remove from subscription list. I my use case I also always generate initial response for subscription (like in live queries) and thus i can use initial response generation to subscribe to all event sources (instead of NotifySubscribe).

C. The library always wraps objects and scalars in awaitable future (and then awaits them) even when result is returned by value. This causes ridiculous overhead when creating scalar arrays. So far i haven't found a use case for `AwaitableObject/AwaitableScalar`

3.
> Yeah, unfortunately C++ just can't do it as simply. I haven't used Golang or its GraphQL frameworks, but I am familiar with a couple of Rust GraphQL frameworks, and a lot of their ease-of-use comes from procedural macros. There are a few proposals out there for static reflection in C++ or meta-classes that could do something similar, but AFAIK those are many years away from becoming part of the standard.
> 
> I think static code generators are the closest we can get to hiding the ugly details from C++.

I agree. And also since all fields originate from schema there will always be a need for external tool to parse schema and generate fields. Static reflection can _maybe_ used to reduce amount of generated code. I don't think there will ever be a compile time AST parser that does not kill the compiler.

EDIT:
4. My need for extensibility has led me to move entire generated part of code to external dll. It was actually possible because graphql dictates return value type and it possible to create models implicitly. [My working concept](https://github.com/gitmodimo/cppgraphqlgen/blob/f310946f167d08f691c57783d9f56f9c8bc84519/include/graphqlservice/GraphQLService.h#L595). The only problem is with variant types since we do not know which variant is supposed to be created. I worked this around with the use of additional traits. As a result my entire solution is rid of generated headers. All except XXXSchema.h because I still need enums and input types. Generating Enums and Input types into separate header(separate from what is below [this line](https://github.com/microsoft/cppgraphqlgen/blob/173fb57abb6290edf8609d3faf2514c050aeafd5/samples/today/schema/TodaySchema.h#L235) ) would be useful (also maybe for [this](https://github.com/microsoft/cppgraphqlgen/pull/318)). 
wravery commented 22 hours ago

Actually, I think cppgraphqlgen should be able to be packaged into something like this, But it may be necessary to write a code generation tool to generate the graphql_server class

graphql_server.setAcallback([]() ->asio::await{
co_return;
});
graphql_server.setBcallback([]() ->asio::await {
co_return;
});
graphql_server.handler("/graphql", "POST");

graphql_server.start("0.0.0.0:80");

What you're describing sounds more like a web framework than what cppgraphqlgen does. I haven't tried it myself, but Oat++ sounds like it fits. If you (or anyone else) wanted to try combining them, I think both projects would benefit. I'd be happy to add another link to the README. 😃

wravery commented 20 hours ago

I have been using cppgraphqlgen in production code for about 2 years. Here are some of my thouths:

Great feedback below, thanks!

  1. cppgraphqlgen lacks extensibility. Concept similar to Apollo federation. I think it seems viable to allow joining two graphql::service::Request services as follows: ... I my opinion it should be possible to generate two separate compilation units/libraries that could be literally added to each other at runtime. ... Addition should expand TypeMap _operations and merge introspection infofmation.

Interesting, yeah. I'm not sure if having schema extensions be handled differently makes sense, but given 2 separate schemas, it should be possible to merge them at the schema level. The only tricky bit, I think, would be figuring out how to dispatch the resolvers to the correct implementation in the merged service at runtime. It might be better to treat this as a new type of service that multiplexes other static services dynamically, including building a merged schema for validation and introspection.

  1. The library is unnecessarily slow because of few reasons:

A. Response is held in graphql::response::Value which is DOM representation that ususally converted to json using additional library. This could be shortcut with service taking visitor instead of returning response. In fact default visitor could be graphql::response::Value builder to keep backward compatibility, but it could be replaced by JsonBuilder based on for example RapidJSON.

As far as converting to JSON goes, there is a visitor pattern already when starting from a response::Value. The response::Writer does this. I'm imagining an API that takes a response::Writer (and maybe a separate visitor to accumulate errors) and passing that to the resolvers to recursively build the results in an alternate form that bypasses response::Value entirely. For instance, clientgen could generate a Writer for the expected Response types. Both of those have some interesting implications for a dynamic multiplexer.

B. There should be more fine grained control for subscriptions (especially NotifySubscribe and NotifyUnsubscribe). I think subscription should return a handle to allow external fine grained control. Ex. In subscription that uses list field it may happen that list changes between NotifySubscribe and NotifyUnsubscribe. In such case NotifyUnsubscribe is useless in "cleaning up" Subscription event sources. I end up using external accounting of tracking the sources and removing then when subscription completes. I want to be able to skip NotifyUnsubscribe step and just remove from subscription list. I my use case I also always generate initial response for subscription (like in live queries) and thus i can use initial response generation to subscribe to all event sources (instead of NotifySubscribe).

I think you're saying that the set of resources/listeners you are tracking to support the subscription might change dynamically, e.g., as items are added or removed from a collection. The approach I'd use for that would probably be to use external bookkeeping for those resources as you do now, but store a handle/ID to those external resources in the RequestState for the subscription. The handler for NotifySubscribe would initialize the external resources, when items are added or removed you can update it externally, and then on NotifyUnsubscribe you'd release everything using the handle to your external resources.

C. The library always wraps objects and scalars in awaitable future (and then awaits them) even when result is returned by value. This causes ridiculous overhead when creating scalar arrays. So far i haven't found a use case for AwaitableObject/AwaitableScalar

One of the changes in the next branch, https://github.com/microsoft/cppgraphqlgen/commit/fa15fb7527dc4d9b2b8a425e2ebb9824a25d2e4d, had a surprisingly big impact on the benchmark programs, at least when running in a debug build (about 20% more throughput using clang-18 or MSVC, coming from reduced time spent resolving requests). It would make sense if the benchmark used any fields with futures that report timeout, which would spawn another thread, but I don't think it does. I think everything should just be deferred rather than async, which should prevent it from suspending and resuming on another thread. My guess is that making it constexpr short-circuited more of the coroutine machinery at compile time.

I haven't checked a release build yet, but I'd be curious to know if this improves your scalar array scenario.

EDIT: 4. My need for extensibility has led me to move entire generated part of code to external dll. It was actually possible because graphql dictates return value type and it possible to create models implicitly. My working concept. The only problem is with variant types since we do not know which variant is supposed to be created. I worked this around with the use of additional traits. As a result my entire solution is rid of generated headers.

I'll have to dig into this some more, but I gather you specialize the GraphQLBuilder for your object types and create a dynamic service::Object implementation from that? Or have you modified other parts of GraphQLService.cpp or SchemaGenerator.cpp to do something more with expected return types, so they're no longer std::shared_ptr<object::Foo>?

All except XXXSchema.h because I still need enums and input types. Generating Enums and Input types into separate header(separate from what is below this line ) would be useful (also maybe for this).

Re: splitting the enums and input types into one or more separate headers, I figured I'd keep it simple for now and just include the ...Schema.h file. There's not that much other content in that header, and to use shared types you're almost certainly going to include it anyway.

nqf commented 14 hours ago

Actually, I think cppgraphqlgen should be able to be packaged into something like this, But it may be necessary to write a code generation tool to generate the graphql_server class

graphql_server.setAcallback([]() ->asio::await{
co_return;
});
graphql_server.setBcallback([]() ->asio::await {
co_return;
});
graphql_server.handler("/graphql", "POST");

graphql_server.start("0.0.0.0:80");

What you're describing sounds more like a web framework than what cppgraphqlgen does. I haven't tried it myself, but Oat++ sounds like it fits. If you (or anyone else) wanted to try combining them, I think both projects would benefit. I'd be happy to add another link to the README. 😃

We actually need an easy-to-use graphql service, Both rust and golang are easy to integrate with http servers,What I mean is that cppgraphqlgen doesn't easily fit into the cppweb framework,If this project could be like rust aync-graphql, I think it would be more popular

gitmodimo commented 3 hours ago

Interesting, yeah. I'm not sure if having schema extensions be handled differently makes sense, but given 2 separate schemas, it should be possible to merge them at the schema level. The only tricky bit, I think, would be figuring out how to dispatch the resolvers to the correct implementation in the merged service at runtime. It might be better to treat this as a new type of service that multiplexes other static services dynamically, including building a merged schema for validation and introspection.

I think it is called Schema stitching. It seems like easiest route for runtime extensibility. It does not cover type extension but it can be emulated by replacing different static services dynamically.

As far as converting to JSON goes, there is a visitor pattern already when starting from a response::Value. The response::Writer does this.

yes and I reckon the response::Value building part can also be redesigned into visitor pattern.

I'm imagining an API that takes a response::Writer (and maybe a separate visitor to accumulate errors) and passing that to the resolvers to recursively build the results in an alternate form that bypasses response::Value entirely.

Exactly!

For instance, clientgen could generate a Writer for the expected Response types.

And the writer would build response::Value or JSON or even act directly on visited values depending on user provider visitor.

Both of those have some interesting implications for a dynamic multiplexer.

I think it all boils down to proper SAX style writer API

One of the changes in the next branch, fa15fb7, had a surprisingly big impact on the benchmark programs, at least when running in a debug build (about 20% more throughput using clang-18 or MSVC, coming from reduced time spent resolving requests). It would make sense if the benchmark used any fields with futures that report timeout, which would spawn another thread, but I don't think it does. I think everything should just be deferred rather than async, which should prevent it from suspending and resuming on another thread. My guess is that making it constexpr short-circuited more of the coroutine machinery at compile time.

I will definitely check this out. With my custom changes i did nou have time to port them to newer library verion

I'll have to dig into this some more, but I gather you specialize the GraphQLBuilder for your object types and create a dynamic service::Object implementation from that? Or have you modified other parts of GraphQLService.cpp or SchemaGenerator.cpp to do something more with expected return types, so they're no longer std::shared_ptr<object::Foo>?

No there is no specialiation of GraphQLBuilder and no other changes AFAIR. The AwaitableObject contrustor handles 3 cases: 1. U&& value is the generated object model (just like in current library implementation)

  1. U&& value is not generated model but can be used to create one. The type of the object model is known as T.
  2. Nr 2 but future

The GraphQLBuilder handles building of the model T given U. It handles:

  1. Nullable as optional and shared_ptr
  2. All sorts of vectors recursively
  3. Unions - This is the tricky one because given T we can only know Union type not specific model type to create. This is where I use additional trait that maps user class Foo to object::Foo. Ex: Graphql:
    union UnionTypeAB = TypeA | TypeB

Field accessor returns graphql::service::Union<TypeA, TypeB> And with custom mapping trait defined the GraphQLBuilder can build desired object model

template <>
struct GraphQLUnion<graphql::service::Union<TypeA, TypeB>, graphql::GQL::object::UnionTypeAB> : std::true_type
{
  using model_map = type_map<pair<TypeA, graphql::GQL::object::TypeA>,
                             pair<TypeB, graphql::GQL::object::TypeB>>;
};

I purposely decided to use external GraphQLUnion trait class instead of creating mapping inside TypeA/TypeB classes to be able to compile all user code without including any of generated class headers. That gives me the ability to generate multiple different schemas, reuse the objects and even adjust union mappings where necessary. The only header that I use in user code is XXXSchema.h cause I need definitions of the inputs and enums. And the ugly part is that I also need the implementations of Input types in XXXSchema.cpp to link my user code. And due to XXXSchema.cpp implements also Operations it pulls in all the generated headers. If input types and enums were in separate header i could reuse the same types header for multiple different schemas as long as they all use subset of "full schema" types.

At this moment my build flow looks like this:

  1. generate schema with all features enabled
  2. use fully featured XXXSchema.h in project build without any actual graphqlservice.
  3. generate schema for every subset of features
  4. for each subset feature schema build dll with graphqlservice using object implementations of 2. Each subset can have individual union mapping defined inn their compilation unit. Type mapping is handled by graphql and concepts are verified during dll compilation.

This solution would work nicely if only there we something equivalent to schemagen --shared-types and schemagen --only-types This would allow to separately develop different features in separate schema files (assuming no naming collisions). Each feature would generate its own "types.h" header and use it. Then as a final step merge all the schmea-parts that are desired and generate specialized service including desired feature-set. My dream is to be able to do it in runtime, but for now it is next best thing.

It does not fully give extensibility but it is somewhere in between.

Re: splitting the enums and input types into one or more separate headers, I figured I'd keep it simple for now and just include the ...Schema.h file. There's not that much other content in that header, and to use shared types you're almost certainly going to include it anyway.

I am including it. The problem is it pulls all other headers from cpp file.