d-exclaimation / pioneer Goto Github PK
View Code? Open in Web Editor NEWGraphQL for Swift.
Home Page: https://pioneer.dexclaimation.com
License: Apache License 2.0
GraphQL for Swift.
Home Page: https://pioneer.dexclaimation.com
License: Apache License 2.0
I've noticed using GraphiQL that the request headers are not being sent for subscriptions. When I generate the Context I use the headers to get an access token, but the headers I defined within GraphiQL are not getting sent. The headers do work for queries and mutations.
Is there a better alternative or is this a bug?
Describe the bug
Currently when a resolver thrown an error, it will use that operation's Response status code to send back a GraphQLResult with the thrown error, but leave out all other things set on it (i.e. headers, cookies, etc.).
It should not make the decision that an error meant the headers, cookies should not be set, and instead use the Response and all its values to encode the GraphQLResult with the error message.
Steps to reproduce
Have a resolver that throw an error after settings values in the response header.
extension Resolver {
func doSomething(ctx: Context, _: NoArguments) throws -> String {
ctx.response.headers.add(name: .init("X-Some-Header"), value: "something")
throw Abort(.notImplemented, reason: "Expected error")
}
}
Have the schema with this as mutation or query, so it can run on HTTP
Schema<Resolver, Context> {
Query { ... }
Mutation {
Field("doSomething", at: Resolver.doSomething)
}
}
Run the server and make a request with the GraphQL query of:
mutation {
doSomething
}
It should return a JSON of GraphQLResult with an empty data
field and errors
field with the thrown error (formatted as GraphQLError), but it will not send back any headers.
When using GraphQL Playground with any schema and configuration that enable subscription, GraphQL Playground will throw an error when attempting to make a subscription request.
Fixing this might not be a huge concern as the playground option has been deprecated. Considering putting the fix with a larger update
Is your feature request related to a problem? Please describe.
Currently for POST request, the max body collection size is 16KB, which can be insufficient with large GraphQL queries
Describe the solution you'd like
Having the option to pass in HTTPBodyStreamStrategy either when constructing Pioneer or when apply to RouteBuilder.
Additional context
This should only be applied for POST request
The current usage of Timer seems to be invalid and thus not firing the callback properly. Hence, no keep-alive messages were sent. Technically, clients have features to reconnect by itself, but it's probably best to actually send the messages.
There should also be a configuration for turning this off when necessary
I have been the sole maintainer of the library for a while. I think I should start making things easier for new contributors to contribute to this library.
I have plans to add a contributing guide and some more details into the library in order so it's easy for someone new to make improvements to the codebase
The internal of Pioneer could be accepting lower level GraphQLSchema
, which open up the options to use whatever GraphQL schema library that was built on the same base library.
On top of that keep a convenient initializer with Graphiti's Schema
so the API wouldn't have to change too much.
Need further testing to figure out differences and caveats
Similar to apollo-server
, it would make sense for Pioneer to be server library agnostic or at least support multiple server libraries.
This can be achieved by separating the core part of the library (GraphQL execution, Subscriptions Handler, GraphQL data structures) and the vapor part (HTTP Handlers, Websocket Handlers, Request and Response extensions), and later adding support for other libraries.
However, this can be quite a big undertaking. So to tackle this, I should keep maintaining the v0.*
of the library with additional small feature updates and patches.
While under a separate branch, start working on separating the core and vapor part of the library.
If the separation is successful, I can continue by adding support in form of packages to other server libraries that are actively maintained and support Swift 5.5 properly.
I have no time estimate on how long this would take, and no deadline when this would start either.
Apollo Sandbox now allows you to embed Apollo Sandbox on your own website and localhost. This could be a nice option to add support for this and it may prevent needing to make CORS changes. I would imagine it would be a similar process to how GraphiQL and the localhost version of BananaCakePop are hosted.
https://www.apollographql.com/blog/tooling/graphql-ide/how-to-use-apollo-sandbox-on-your-localhost/
Async/await resolvers for non-encodable return type like Protocols.
public extension Graphiti.Field {
/// Async-await non-throwing GraphQL resolver function
typealias AsyncAwaitResolve<ObjectType, Context, Arguments, FieldType> = (ObjectType) -> (Context, Arguments) async -> FieldType
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
@ArgumentComponentBuilder<Arguments> _ argument: () -> [ArgumentComponent<Arguments>]
) {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task { await function(type)(context, arguments) }
}
}
self.init(name, at: resolve, argument)
}
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
) where Arguments == NoArguments {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task { await function(type)(context, arguments) }
}
}
self.init(name, at: resolve, {})
}
/// Async-await throwing GraphQL resolver function
typealias AsyncAwaitThrowingResolve<ObjectType, Context, Arguments, FieldType> = (ObjectType) -> (Context, Arguments) async throws -> FieldType
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
@ArgumentComponentBuilder<Arguments> _ argument: () -> [ArgumentComponent<Arguments>]
) {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task { try await function(type)(context, arguments) }
}
}
self.init(name, at: resolve, argument)
}
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
) where Arguments == NoArguments {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task { try await function(type)(context, arguments) }
}
}
self.init(name, at: resolve, {})
}
/// Async-await non-throwing GraphQL resolver function
typealias AsyncAwaitResolveWithEventLoop<ObjectType, Context, Arguments, FieldType> = (ObjectType) -> (Context, Arguments, EventLoopGroup) async -> FieldType
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
@ArgumentComponentBuilder<Arguments> _ argument: () -> [ArgumentComponent<Arguments>]
) {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task {
await function(type)(context, arguments, eventLoopGroup)
}
}
}
self.init(name, at: resolve, argument)
}
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
) where Arguments == NoArguments {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task {
await function(type)(context, arguments, eventLoopGroup)
}
}
}
self.init(name, at: resolve, {})
}
/// Async-await throwing GraphQL resolver function
typealias AsyncAwaitThrowingResolveWithEventLoop<ObjectType, Context, Arguments, FieldType> = (ObjectType) -> (Context, Arguments, EventLoopGroup) async throws -> FieldType
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
@ArgumentComponentBuilder<Arguments> _ argument: () -> [ArgumentComponent<Arguments>]
) {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task {
try await function(type)(context, arguments, eventLoopGroup)
}
}
}
self.init(name, at: resolve, argument)
}
convenience init<ResolveType>(
_ name: String,
at function: @escaping AsyncAwaitResolve<ObjectType, Context, Arguments, ResolveType>,
as: FieldType.Type,
) where Arguments == NoArguments {
let resolve: AsyncResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments, eventLoopGroup in
eventLoopGroup.task {
try await function(type)(context, arguments, eventLoopGroup)
}
}
}
self.init(name, at: resolve, {})
}
}
How can I add something to the response? For example, I need to add cookies
Immutable data structures aren't Sendable
by default. ID
is a built-in types that is immutable and should be safely copied across asynchronous code should conforms to Sendable
At the moment, all the documentation is cluttered in the README. I should create a documentation website and tidy things up
Considering adding options to enable an optional CSRF and XS-Search timing attacks prevention, which can be done as another option for HTTPStrategy.
At the moment, the only measurement to prevent those attacks are setting HTTP Strategy to either .queryOnlyGet
, .onlyPost
, or splitQueryAndMutation
, and ensure that only mutations can have side effects, which may be insufficient.
Graphiti has Connection for adding pagination, can it be used somehow with QueryBuilder?
GraphQLSwift/GraphQL has provided a custom JSONEncoder to maintain order of the JSON fields. Might want to look into this and see if can be applied.
Adding a FAQ page may help make it easier to work with this package. It can be used to compile all common questions and the answers.
My primary use case is to include an access token or user session token for a given user, but there could be other use cases. Is this possible right now?
This is probably one of those features which needs adopting at both the Graphiti level and in Pioneer but it would be awesome if we could create our own directives.
The most obvious and helpful one would be some form of Auth directive documented here. This could feed into Vapor's authentication solution to allow us to only return certain types or fields if the user is authenticated to view them.
This is kind of possible loosely with the validation rules released in #72, however that has a couple flaws:
I believe this may live partially here and partially in the dependencies this package relies on but it would be amazing for production use cases if we were able to trace the performance of queries, down to the individual resolvers and such.
There are many packages relying on swift-nio EventLoopGroup
, like data loader and database orms. Given that Pioneer will pass that from the request. Async await variant for resolver should still provide the EventLoopGroup
as the third parameter.
Alternatively, the async resolver should get its EventLoopGroup
from the context given that it has access to the context.
Bringing it back
struct Resolver {
func hello(ctx: (Request, Response), arg: NoArgument, group: EventLoopGroup) async -> String {}
}
Using context
struct Resolver {
func hello(ctx: (Request, Response), arg: NoArgument) async -> String {
let group = ctx.0.eventLoopGroup
}
}
Is your feature request related to a problem? Please describe.
It will be nice to have a middleware-like feature but are for individuals resolver basis and not the entirety of server level.
Describe the solution you'd like
Field
to allow middlewareField("field1", at: Resolve.field1, use: [Middleware1, Middleware2, Middleware3]) {
Argument("arg1", at: \.arg1)
}
Field("field1", at: withMiddleware(Resolver.field1, use: Middleware1, Middleware2, Middleware3)) {
Argument("arg1", at: \.arg1)
}
Describe alternatives you've considered
Implement this on Graphiti level
Field("field1", at: Resolve.field1) {
Argument("arg1", at: \.arg1)
}
.middleware(use: Middleware1, Middleware2, Middleware3)
Describe the bug
The ability to intercept a websocket initialisation is not fully implemented yet
I want to highlight that Swift 6 was already set to be a safer version for Swift 5 mainly focusing on erroring out unsafe concurrent code in regards to Sendable
and actor isolation, as this is something to be aware of and probably worked on in the future but not necessary immediately.
Getting into that point is not easy as a lot of packages has yet fully added Sendable
to data structures that may fit. However, periodical checks can be done to remove ones that can be refactored
It might make sense to consider adding something analogous to the following:
https://www.apollographql.com/blog/backend/subscriptions/graphql-subscriptions-with-redis-pub-sub/
While AsyncPubSub
is great for many use cases, in production where we might have multiple server instances, it may be necessary to use something like Redis instead of an in memory pubsub to handle subscriptions. It would be cool if there could be an option to use Redis when configuring Pioneer and it would be even cooler if we could use the same API as AsyncPubSub.
So it would be a simple configuration change for the developer, and then we could switch between redis and the in memory AsyncPubSub without changing our EventStream code.
Do you think this is possible? I know this might be a challenge.
The current configuration is very simple. I went with that option because it's easier and made sense if I were to only use for testing. However, the build has been rather slow and testing took a bit too long.
When I try to get the parent id, I get an error "Fatal error: Cannot access field before it is initialized or fetched: country_id".
I found that all fields are empty except id
extension AddressModel {
func getCountry(context: Context, _: NoArguments, eventLoopGroup: EventLoopGroup) async throws -> CountryModel? {
let id = self.$country.id
return try await context.request.graphQLDataLoader
.countryLoader.load(key: id, on: eventLoopGroup).get()
}
}
If any subscription resolver throws an error, it would be converted to GraphQLError
and capture its description. Then, server sent an Error
/ GQL_ERROR
with that error.
[
{
"type": "error",
"id": "...",
"payload": [
{ "message": "Internal server error" }
]
}
]
or sent a Next
/ GQL_DATA
then with Complete
/ GQL_COMPLETE
immediately.
[
{
"type": "next",
"id": "...",
"payload": {
"data": null,
"errors": [ { "message": "Internal server error" } ]
}
},
{
"type": "complete",
"id": "...",
}
]
If a subscription resolver throws an error. Its description will not be captured and the server sent a Next
/ GQL_DATA
message with a generic error message, and not end the subscription.
[
{
"type": "next",
"id": "...",
"payload": {
"data": null,
"errors": [ { "message": "Internal server error" } ]
}
}
]
EventLoopFuture
and put the error description into the GraphQL Error messageproto.next
to proto.error
or send a completion message afterwardswebsocketProtocol
to graphqlWs
nil
graphql-ws
queryOnlyGet
(default)Describe the bug
When an AbortError
is thrown at any point of the code the headers
values are always ignored, and thus meant nothing.
To Reproduce
Steps to reproduce the behavior:
Abort
or any AbortError
with some headers
Expected behavior
Expected that the headers
is set to the response headers.
Desktop (please complete the following information):
For applications which seek the ability to charge for access to their GraphQL API, a cost analysis must be done to charge request based on their impact. As in, requesting a single model will cost less than multiple models with relationships and such.
It would be good if we could provide a way of intercepting requests before they're executed. This would allow us to throw an error (also helpful for auth), record usage, or even add support for a "dryRun" mode which simply returns the query cost without actually executing it.
Examples:
Considering an asynchronous version for contextBuilder. However, it might be overcomplicated and user of the library should instead opt for leaving a EventLoopFuture
or Task
properties instead
When connecting through websocket on an specified interval, a keep-alive message was supposed to be sent with the type of Ping
and the form of
{
"type": "ping"
}
Message of the type of ConnectionAck
and this form was sent instead on that same interval
{
"type": "connection_ack"
}
At GraphQLWs.swift
line 66, change the ConnectionAck constant to Ping
static var keepAliveMessage: String { GraphQLMessage(type: Ping).jsonString }
websocketProtocol
to graphqlWs
nil
graphql-ws
queryOnlyGet
(default)I've noticed in both GraphiQL and BananaCakePop that if include both a named subscription and named mutation operation, that the mutation always fails when it is run. I'm interested if anyone else has this issue. When I remove the subscription, the mutation works fine.
subscription NewMessage {
newMessage {
...message
}
}
mutation CreateMessage {
createMessage(input: {chatId: "aaa", text: "Hello there!"}) {
message {
...message
}
}
If I try to run CreateMessage
it fails. If I remove the subscription, it succeeds. In both cases, the subscription runs fine.
The error I get is a 500 code error.
"{\"error\":true,\"reason\":\"The operation couldn’t be completed. (Pioneer.Pioneer<MyApp.MainAPI, MyApp.UserContext>.ResolveError error 0.)\"}"
I haven't add this problem with other servers, so I suspect this is a Pioneer specific bug with how Pioneer decodes the operations.
Describe the bug
Documentation search feature is able to find the proper documentation topic but points to an incorrect path which always goes to a 404 page.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
Search should points to an actual page, most of the documentation is under the /docs
route.
Desktop (please complete the following information):
Additional context
Seemed like the path is partially correct just missing the /docs
route, must investigate why and how to fix it.
Async-await extensions for DataLoader
By cancelling a task and forcing conversion to AsyncStream
, the need for using both Nozzle
and EventNozzle
is minimal at best.
I should opt for using built-in features as it leave the optimization to the standard library maintainers and contributors.
I don't need to rip out all of the Desolate implementation, but it keep it as a place where I can delegate the work to other implementation that was already maintained.
AsyncStream
and then AsyncEventStream
AsyncStream
and Task
with cancelation in mindIs your feature request related to a problem? Please describe.
It's not a pleasant experience having to work with Map
for the WebSocket payload, especially if the user already know what to expect.
Describe the solution you'd like
2 approach:
Map
with functionalities to be parsed to other typesSimilar to graphql-subscriptions
package, I should probably provide a simple data structure for handling in memory multiple topic event streaming.
I can do this easily with multiple Jets and a single Actor.
Describe the bug
So my team is using Apollo iOS on our frontend and Pioneer on the backend. I'm noticing that when we pause the websocket connection on the frontend (i.e., when backgrounding the app) that we are unable to reconnect the websocket later on. Perhaps this is an issue with Apollo iOS, but given that no one else is having this issue while using ApolloServer, I suspect this is a bug within Pioneer.
Front end code:
print(transport.isConnected()) // Prints true
transport.pauseWebSocketConnection()
print(transport.isConnected()) // Prints false
transport.resumeWebSocketConnection(autoReconnect: true)
print(transport.isConnected()) // Prints false (should print true)
We're using Pioneer 1.0 with the defaults and the latest version of Apollo iOS.
Is your feature request related to a problem? Please describe.
Currently, there's no way to check if a WebSocket connection has a valid payload until an operation is executed. Also if a connection never send an init operation while connecting, while that connection wouldn't be able to make any request, there's no timeout for that connection.
Describe the solution you'd like
The current unit test is functional but it's at a pretty bad state where it's hard to work with and understand. It's important that this test cases are organised and structured better so that they don't just become useless code noises and actually mean something.
Describe the bug
As per the GraphQL specification the order of the returned data keys should match that of those provided in the query. Currently, it's randomised.
From looking into the code this appears to be caused by this line:
try res.content.encode(result)
On line 73 of the Pioneer+Http file. By default, this line will use the default JSONEncoder which is incorrect. We should be using the GraphQLJSONEncoder which is provided by the underlying GraphQL library.
It would also be nice if you gave us the ability to customise this encoder when we initialise the Pioneer struct.
Confirmed on v0.9.4
Given that GraphQL Playground has been retired and that GraphiQL already have support for adding subscription with both protocols.
GraphQL Playground should be removed now to GraphIQL instead.
<!DOCTYPE html>
<html>
<head>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<!--
This GraphiQL example depends on Promise and fetch, which are available in
modern browsers, but can be "polyfilled" for older browsers.
GraphiQL itself depends on React DOM.
If you do not want to rely on a CDN, you can host these files locally or
include them directly in your favored resource bunder.
-->
<script
crossorigin
src="https://unpkg.com/react@16/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"
></script>
<!--
These two files can be found in the npm module, however you may wish to
copy them directly into your environment, or perhaps include them in your
favored resource bundler.
-->
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
</head>
<body>
<div id="graphiql">Loading...</div>
<script src="https://unpkg.com/graphiql/graphiql.min.js" type="application/javascript"></script>
<!--
This line add subscriptions-transport-ws for better protocol options
-->
<script src="https://unpkg.com/subscriptions-transport-ws/browser/client.js" type="application/javascript"></script>
<script>
const url = 'http://localhost:4000/graphql';
const subscriptionUrl = 'ws://localhost:4000/graphql/websocket';
const subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionUrl, { reconnect: true });
const fetcher = GraphiQL.createFetcher({
url,
subscriptionUrl,
});
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher
}),
document.getElementById('graphiql'),
);
</script>
</body>
</html>
Describe the bug
Given that Graphiti now supports async resolver. The current extensions given in Pioneer now produce a problem with ambiguity.
Solutions
Removing any async extensions for Graphiti altogether (excluding some which may come in handy).
Related PR
One common example I could see is if the context requires verifying and decoding a JWT access token or if before the context is created a query needs to be made to a database and if the user doesn't exist it should throw an error.
To my knowledge this is not possible right now.
I think it's a relatively simple change in the handle
function in Pioneer.swift. The context would just have to be created before executeGraphQL.
Is your feature request related to a problem? Please describe.
It would be great if Pioneer can pass some if not most of the optional spec requirement for GraphQL over HTTP.
Describe the solution you'd like
The changes shouldn't break any existing behaviour of Pioneer and shouldn't require anything done from the user side.
Describe alternatives you've considered
We are slightly at the same compliant as Apollo Server which is technically is enough, but if the optional requirements aren't causing any issues, it should be fine to implement it
Additional context
I'm having a weird bug happen on AWS where a subscription only publishes every other time that a certain mutation is performed. It does not do this when I run the server locally on localhost.
One thing I realized that may be causing this is that I'm using the in memory AsyncPubSub
. It's possible that the server may be executing calls on 2 different instances. This may mean that the client is subscribing to one instance's subscriptions, but that the load balancer causes the mutations to switch off between different instances, so only half the time would the server with the open websocket be receiving the mutation and publishing it.
I will try to come up with a solution using Redis and the new PubSub
protocol, but I wanted to get someone else's thoughts on this.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.