Skip to content

Commit

Permalink
Merge branch 'master' into graphql-operation-builder
Browse files Browse the repository at this point in the history
hgiasac committed Feb 26, 2024
2 parents 3784341 + f578304 commit 77f1576
Showing 32 changed files with 1,779 additions and 1,083 deletions.
25 changes: 21 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -16,13 +16,17 @@ jobs:
runs-on: ubuntu-20.04
permissions:
pull-requests: write
# Required: allow read access to the content for analysis.
contents: read
# Optional: Allow write access to checks to allow the action to annotate code in the PR.
checks: write
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-go@v4
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.20"
- uses: actions/cache@v3
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
@@ -42,14 +46,27 @@ jobs:
run: |
cd ./example/hasura
docker-compose up -d
- name: Lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
only-new-issues: true
skip-cache: false
- name: Run Go unit tests for example/subscription
run: |
cd example/subscription
go get -t -v ./...
go test -v -race -timeout 3m ./...
- name: Run Go unit tests
run: go test -v -race -timeout 3m -coverprofile=coverage.out ./...
- name: Go coverage format
if: ${{ github.event_name == 'pull_request' }}
run: |
go get github.com/boumenot/gocover-cobertura
go install github.com/boumenot/gocover-cobertura
gocover-cobertura < coverage.out > coverage.xml
- name: Code Coverage Summary Report
if: ${{ github.event_name == 'pull_request' }}
uses: irongut/[email protected]
with:
filename: coverage.xml
@@ -63,7 +80,7 @@ jobs:
thresholds: "60 80"
- name: Add Coverage PR Comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request_target' }}
if: ${{ github.event_name == 'pull_request' }}
with:
path: code-coverage-results.md
- name: Dump docker logs on failure
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.idea/
coverage.out
coverage.out
go.work
go.work.sum
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -571,6 +571,16 @@ client := graphql.NewSubscriptionClient("wss://example.com/graphql").
})
```
Some servers validate custom auth tokens on the header instead. To authenticate with headers, use `WebsocketOptions`:
```go
client := graphql.NewSubscriptionClient(serverEndpoint).
WithWebSocketOptions(graphql.WebsocketOptions{
HTTPHeader: http.Header{
"Authorization": []string{"Bearer random-secret"},
},
})
```
#### Options
33 changes: 33 additions & 0 deletions example/graphql-ws-bc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Subscription example with graphql-ws backwards compatibility

The example demonstrates the subscription client with the native graphql-ws Node.js server, using [ws server usage with subscriptions-transport-ws backwards compatibility](https://the-guild.dev/graphql/ws/recipes#ws-server-usage-with-subscriptions-transport-ws-backwards-compatibility) and [custom auth handling](https://the-guild.dev/graphql/ws/recipes#server-usage-with-ws-and-custom-auth-handling) recipes. The client authenticates with the server via HTTP header.

```go
client := graphql.NewSubscriptionClient(serverEndpoint).
WithWebSocketOptions(graphql.WebsocketOptions{
HTTPHeader: http.Header{
"Authorization": []string{"Bearer random-secret"},
},
})
```

## Get started

### Server

Requires Node.js and npm

```bash
cd server
npm install
npm start
```

The server will be hosted on `localhost:4000`.

### Client

```bash
go run ./client
```

85 changes: 85 additions & 0 deletions example/graphql-ws-bc/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// subscription is a test program currently being used for developing graphql package.
// It performs queries against a local test GraphQL server instance.
//
// It's not meant to be a clean or readable example. But it's functional.
// Better, actual examples will be created in the future.
package main

import (
"flag"
"log"
"net/http"

graphql "github.com/hasura/go-graphql-client"
)

func main() {
protocol := graphql.GraphQLWS
protocolArg := flag.String("protocol", "graphql-ws", "The protocol is used for the subscription")
flag.Parse()

if protocolArg != nil {
switch *protocolArg {
case "graphql-ws":
case "":
case "ws":
protocol = graphql.SubscriptionsTransportWS
default:
panic("invalid protocol. Accept [ws, graphql-ws]")
}
}

if err := startSubscription(protocol); err != nil {
panic(err)
}
}

const serverEndpoint = "http://localhost:4000"

func startSubscription(protocol graphql.SubscriptionProtocolType) error {
log.Printf("start subscription with protocol: %s", protocol)
client := graphql.NewSubscriptionClient(serverEndpoint).
WithWebSocketOptions(graphql.WebsocketOptions{
HTTPHeader: http.Header{
"Authorization": []string{"Bearer random-secret"},
},
}).
WithLog(log.Println).
WithProtocol(protocol).
WithoutLogTypes(graphql.GQLData, graphql.GQLConnectionKeepAlive).
OnError(func(sc *graphql.SubscriptionClient, err error) error {
log.Print("err", err)
return err
})

defer client.Close()

/*
subscription {
greetings
}
*/
var sub struct {
Greetings string `graphql:"greetings"`
}

_, err := client.Subscribe(sub, nil, func(data []byte, err error) error {

if err != nil {
log.Println(err)
return nil
}

if data == nil {
return nil
}
log.Printf("hello: %+v", string(data))
return nil
})

if err != nil {
panic(err)
}

return client.Run()
}
1 change: 1 addition & 0 deletions example/graphql-ws-bc/server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
86 changes: 86 additions & 0 deletions example/graphql-ws-bc/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// The example is copied from ws server usage with subscriptions-transport-ws backwards compatibility example
// https://the-guild.dev/graphql/ws/recipes#ws-server-usage-with-subscriptions-transport-ws-backwards-compatibility

import http from "http";
import { WebSocketServer } from "ws"; // yarn add ws
// import ws from 'ws'; yarn add ws@7
// const WebSocketServer = ws.Server;
import { execute, subscribe } from "graphql";
import { GRAPHQL_TRANSPORT_WS_PROTOCOL } from "graphql-ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { SubscriptionServer, GRAPHQL_WS } from "subscriptions-transport-ws";
import { schema } from "./schema";

// extra in the context
interface Extra {
readonly request: http.IncomingMessage;
}

// your custom auth
class Forbidden extends Error {}
function handleAuth(request: http.IncomingMessage) {
// do your auth on every subscription connect
const token = request.headers["authorization"];

// or const { iDontApprove } = session(request.cookies);
if (token !== "Bearer random-secret") {
// throw a custom error to be handled
throw new Forbidden(":(");
}
}

// graphql-ws
const graphqlWs = new WebSocketServer({ noServer: true });
useServer(
{
schema,
onConnect: async (ctx) => {
// do your auth on every connect (recommended)
await handleAuth(ctx.extra.request);
},
},
graphqlWs
);

// subscriptions-transport-ws
const subTransWs = new WebSocketServer({ noServer: true });
SubscriptionServer.create(
{
schema,
execute,
subscribe,
},
subTransWs
);

// create http server
const server = http.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});

// listen for upgrades and delegate requests according to the WS subprotocol
server.on("upgrade", (req, socket, head) => {
// extract websocket subprotocol from header
const protocol = req.headers["sec-websocket-protocol"];
const protocols = Array.isArray(protocol)
? protocol
: protocol?.split(",").map((p) => p.trim());

// decide which websocket server to use
const wss =
protocols?.includes(GRAPHQL_WS) && // subscriptions-transport-ws subprotocol
!protocols.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL) // graphql-ws subprotocol
? subTransWs
: // graphql-ws will welcome its own subprotocol and
// gracefully reject invalid ones. if the client supports
// both transports, graphql-ws will prevail
graphqlWs;
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
});

const port = 4000;
console.log(`listen server on localhost:${port}`);
server.listen(port);
326 changes: 326 additions & 0 deletions example/graphql-ws-bc/server/package-lock.json
21 changes: 21 additions & 0 deletions example/graphql-ws-bc/server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "server",
"version": "1.0.0",
"description": "graphql-ws backward compatibility server example",
"main": "index.ts",
"scripts": {
"start": "ts-node index.ts"
},
"license": "MIT",
"dependencies": {
"graphql": "^16.8.1",
"graphql-ws": "^5.14.3",
"subscriptions-transport-ws": "^0.11.0",
"ws": "^8.16.0"
},
"devDependencies": {
"@types/ws": "^8.5.10",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
36 changes: 36 additions & 0 deletions example/graphql-ws-bc/server/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql";

/**
* Construct a GraphQL schema and define the necessary resolvers.
*
* type Query {
* hello: String
* }
* type Subscription {
* greetings: String
* }
*/
export const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: "Query",
fields: {
hello: {
type: GraphQLString,
resolve: () => "world",
},
},
}),
subscription: new GraphQLObjectType({
name: "Subscription",
fields: {
greetings: {
type: GraphQLString,
subscribe: async function* () {
for (const hi of ["Hi", "Bonjour", "Hola", "Ciao", "Zdravo"]) {
yield { greetings: hi };
}
},
},
},
}),
});
109 changes: 109 additions & 0 deletions example/graphql-ws-bc/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */

/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */

/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */

/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */

/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */

/* Type Checking */
"strict": false, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */

/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
15 changes: 15 additions & 0 deletions example/graphqldev/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/hasura/go-graphql-client/example/graphqldev

go 1.20

require (
github.com/graph-gophers/graphql-go v1.5.0
github.com/hasura/go-graphql-client v0.11.0
)

require (
github.com/google/uuid v1.6.0 // indirect
nhooyr.io/websocket v1.8.10 // indirect
)

replace github.com/hasura/go-graphql-client => ../../
21 changes: 21 additions & 0 deletions example/graphqldev/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc=
github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
4 changes: 3 additions & 1 deletion example/hasura/client/graphql-ws/client.go
Original file line number Diff line number Diff line change
@@ -20,7 +20,9 @@ const (

func main() {
go insertUsers()
startSubscription()
if err := startSubscription(); err != nil {
panic(err)
}
}

func startSubscription() error {
6 changes: 4 additions & 2 deletions example/hasura/client/subscriptions-transport-ws/client.go
Original file line number Diff line number Diff line change
@@ -19,7 +19,9 @@ const (

func main() {
go insertUsers()
startSubscription()
if err := startSubscription(); err != nil {
panic(err)
}
}

func startSubscription() error {
@@ -73,7 +75,7 @@ func startSubscription() error {
// automatically unsubscribe after 10 seconds
go func() {
time.Sleep(10 * time.Second)
client.Unsubscribe(subId)
_ = client.Unsubscribe(subId)
}()

return client.Run()
14 changes: 0 additions & 14 deletions example/realworld/main.go
Original file line number Diff line number Diff line change
@@ -5,8 +5,6 @@ import (
"encoding/json"
"flag"
"log"
"net/http"
"net/http/httptest"
"os"

graphql "github.com/hasura/go-graphql-client"
@@ -61,15 +59,3 @@ func print(v interface{}) {
panic(err)
}
}

// localRoundTripper is an http.RoundTripper that executes HTTP transactions
// by using handler directly, instead of going over an HTTP connection.
type localRoundTripper struct {
handler http.Handler
}

func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
w := httptest.NewRecorder()
l.handler.ServeHTTP(w, req)
return w.Result(), nil
}
2 changes: 1 addition & 1 deletion example/subscription/client.go
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ func startSubscription() error {
// automatically unsubscribe after 10 seconds
go func() {
time.Sleep(10 * time.Second)
client.Unsubscribe(subId)
_ = client.Unsubscribe(subId)
}()

return client.Run()
17 changes: 17 additions & 0 deletions example/subscription/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module github.com/hasura/go-graphql-client/example/subscription

go 1.20

require (
github.com/graph-gophers/graphql-go v1.5.0
github.com/graph-gophers/graphql-transport-ws v0.0.2
github.com/hasura/go-graphql-client v0.11.0
)

require (
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.4.1 // indirect
nhooyr.io/websocket v1.8.10 // indirect
)

replace github.com/hasura/go-graphql-client => ../../
30 changes: 30 additions & 0 deletions example/subscription/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc=
github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo=
github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
4 changes: 3 additions & 1 deletion example/subscription/main.go
Original file line number Diff line number Diff line change
@@ -8,5 +8,7 @@ package main
func main() {
go startServer()
go startSendHello()
startSubscription()
if err := startSubscription(); err != nil {
panic(err)
}
}
559 changes: 559 additions & 0 deletions example/subscription/subscription_test.go

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion example/tibber/client.go
Original file line number Diff line number Diff line change
@@ -16,7 +16,9 @@ const (
)

func main() {
startSubscription()
if err := startSubscription(); err != nil {
panic(err)
}
}

// the subscription uses the Real time subscription demo
14 changes: 1 addition & 13 deletions go.mod
Original file line number Diff line number Diff line change
@@ -3,18 +3,6 @@ module github.com/hasura/go-graphql-client
go 1.20

require (
github.com/google/uuid v1.5.0
github.com/graph-gophers/graphql-go v1.5.0
github.com/graph-gophers/graphql-transport-ws v0.0.2
github.com/google/uuid v1.6.0
nhooyr.io/websocket v1.8.10
)

require (
github.com/gorilla/websocket v1.5.1 // indirect
github.com/klauspost/compress v1.17.4 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.16.0 // indirect
)

replace github.com/gin-gonic/gin v1.6.3 => github.com/gin-gonic/gin v1.9.1
116 changes: 2 additions & 114 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,116 +1,4 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc=
github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo=
github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b h1:Qwe1rC8PSniVfAFPFJeyUkB+zcysC3RgJBAGk7eqBEU=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
25 changes: 21 additions & 4 deletions graphql.go
Original file line number Diff line number Diff line change
@@ -154,7 +154,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
resp, err := c.httpClient.Do(request)

if c.debug {
reqReader.Seek(0, io.SeekStart)
_, _ = reqReader.Seek(0, io.SeekStart)
}

if err != nil {
@@ -206,7 +206,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
err = json.NewDecoder(r).Decode(&out)

if c.debug {
respReader.Seek(0, io.SeekStart)
_, _ = respReader.Seek(0, io.SeekStart)
}

if err != nil {
@@ -332,22 +332,38 @@ type Error struct {
Line int `json:"line"`
Column int `json:"column"`
} `json:"locations"`
Path []interface{} `json:"path"`
err error
}

// Error implements error interface.
func (e Error) Error() string {
return fmt.Sprintf("Message: %s, Locations: %+v, Extensions: %+v", e.Message, e.Locations, e.Extensions)
return fmt.Sprintf("Message: %s, Locations: %+v, Extensions: %+v, Path: %+v", e.Message, e.Locations, e.Extensions, e.Path)
}

// Unwrap implement the unwrap interface.
func (e Error) Unwrap() error {
return e.err
}

// Error implements error interface.
func (e Errors) Error() string {
b := strings.Builder{}
for _, err := range e {
b.WriteString(err.Error())
_, _ = b.WriteString(err.Error())
}
return b.String()
}

// Unwrap implements the error unwrap interface.
func (e Errors) Unwrap() []error {
var errs []error
for _, err := range e {
errs = append(errs, err.err)
}
return errs
}

func (e Error) getInternalExtension() map[string]interface{} {
if e.Extensions == nil {
return make(map[string]interface{})
@@ -366,6 +382,7 @@ func newError(code string, err error) Error {
Extensions: map[string]interface{}{
"code": code,
},
err: err,
}
}

75 changes: 71 additions & 4 deletions graphql_test.go
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ func TestClient_Query_partialDataWithErrorResponse(t *testing.T) {
if err == nil {
t.Fatal("got error: nil, want: non-nil")
}
if got, want := err.Error(), "Message: Could not resolve to a node with the global id of 'NotExist', Locations: [{Line:10 Column:4}], Extensions: map[]"; got != want {
if got, want := err.Error(), "Message: Could not resolve to a node with the global id of 'NotExist', Locations: [{Line:10 Column:4}], Extensions: map[], Path: [node2]"; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}

@@ -110,7 +110,7 @@ func TestClient_Query_partialDataRawQueryWithErrorResponse(t *testing.T) {
if err == nil {
t.Fatal("got error: nil, want: non-nil\n")
}
if got, want := err.Error(), "Message: Could not resolve to a node with the global id of 'NotExist', Locations: [{Line:10 Column:4}], Extensions: map[]"; got != want {
if got, want := err.Error(), "Message: Could not resolve to a node with the global id of 'NotExist', Locations: [{Line:10 Column:4}], Extensions: map[], Path: [node2]"; got != want {
t.Errorf("got error: %v, want: %v\n", got, want)
}
if q.Node1 == nil || string(q.Node1) != `{"id":"MDEyOklzc3VlQ29tbWVudDE2OTQwNzk0Ng=="}` {
@@ -165,7 +165,7 @@ func TestClient_Query_noDataWithErrorResponse(t *testing.T) {
if err == nil {
t.Fatal("got error: nil, want: non-nil")
}
if got, want := err.Error(), "Message: Field 'user' is missing required arguments: login, Locations: [{Line:7 Column:3}], Extensions: map[]"; got != want {
if got, want := err.Error(), "Message: Field 'user' is missing required arguments: login, Locations: [{Line:7 Column:3}], Extensions: map[], Path: []"; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
if q.User.Name != "" {
@@ -215,7 +215,7 @@ func TestClient_Query_errorStatusCode(t *testing.T) {
if err == nil {
t.Fatal("got error: nil, want: non-nil")
}
if got, want := err.Error(), `Message: 500 Internal Server Error; body: "important message\n", Locations: [], Extensions: map[code:request_error]`; got != want {
if got, want := err.Error(), `Message: 500 Internal Server Error; body: "important message\n", Locations: [], Extensions: map[code:request_error], Path: []`; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
if q.User.Name != "" {
@@ -253,6 +253,63 @@ func TestClient_Query_errorStatusCode(t *testing.T) {
}
}

func TestClient_Query_requestError(t *testing.T) {
want := errors.New("bad error")
client := graphql.NewClient("/graphql", &http.Client{Transport: errorRoundTripper{err: want}})

var q struct {
User struct {
Name string
}
}
err := client.Query(context.Background(), &q, nil)
if err == nil {
t.Fatal("got error: nil, want: non-nil")
}
if got, want := err.Error(), `Message: Post "/graphql": bad error, Locations: [], Extensions: map[code:request_error], Path: []`; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
if q.User.Name != "" {
t.Errorf("got non-empty q.User.Name: %v", q.User.Name)
}
if got := err; !errors.Is(got, want) {
t.Errorf("got error: %v, want: %v", got, want)
}

gqlErr := err.(graphql.Errors)
if got, want := gqlErr[0].Extensions["code"], graphql.ErrRequestError; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
if _, ok := gqlErr[0].Extensions["internal"]; ok {
t.Errorf("expected empty internal error")
}
if got := gqlErr[0]; !errors.Is(err, want) {
t.Errorf("got error: %v, want %v", got, want)
}

// test internal error data
client = client.WithDebug(true)
err = client.Query(context.Background(), &q, nil)
if err == nil {
t.Fatal("got error: nil, want: non-nil")
}
if !errors.As(err, &graphql.Errors{}) {
t.Errorf("the error type should be graphql.Errors")
}
gqlErr = err.(graphql.Errors)
if got, want := gqlErr[0].Message, `Post "/graphql": bad error`; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
if got, want := gqlErr[0].Extensions["code"], graphql.ErrRequestError; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
interErr := gqlErr[0].Extensions["internal"].(map[string]interface{})

if got, want := interErr["request"].(map[string]interface{})["body"], "{\"query\":\"{user{name}}\"}\n"; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
}

// Test that an empty (but non-nil) variables map is
// handled no differently than a nil variables map.
func TestClient_Query_emptyVariables(t *testing.T) {
@@ -425,6 +482,16 @@ func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
return w.Result(), nil
}

// errorRoundTripper is an http.RoundTripper that always returns the supplied
// error.
type errorRoundTripper struct {
err error
}

func (e errorRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) {
return nil, e.err
}

func mustRead(r io.Reader) string {
b, err := io.ReadAll(r)
if err != nil {
6 changes: 5 additions & 1 deletion pkg/jsonutil/graphql_test.go
Original file line number Diff line number Diff line change
@@ -508,13 +508,17 @@ func TestUnmarshalGraphQL_unexportedField(t *testing.T) {
type query struct {
foo *string
}
err := jsonutil.UnmarshalGraphQL([]byte(`{"foo": "bar"}`), new(query))
q := new(query)
err := jsonutil.UnmarshalGraphQL([]byte(`{"foo": "bar"}`), q)
if err == nil {
t.Fatal("got error: nil, want: non-nil")
}
if got, want := err.Error(), "struct field for \"foo\" doesn't exist in any of 1 places to unmarshal"; got != want {
t.Errorf("got error: %v, want: %v", got, want)
}
if q.foo != nil {
t.Errorf("expected foo = nil, got: %v", q.foo)
}
}

func TestUnmarshalGraphQL_multipleValues(t *testing.T) {
34 changes: 17 additions & 17 deletions query.go
Original file line number Diff line number Diff line change
@@ -140,9 +140,9 @@ func queryArguments(variables map[string]interface{}) string {

var buf bytes.Buffer
for _, k := range keys {
io.WriteString(&buf, "$")
io.WriteString(&buf, k)
io.WriteString(&buf, ":")
_, _ = io.WriteString(&buf, "$")
_, _ = io.WriteString(&buf, k)
_, _ = io.WriteString(&buf, ":")
writeArgumentType(&buf, reflect.TypeOf(variables[k]), variables[k], true)
// Don't insert a comma here.
// Commas in GraphQL are insignificant, and we want minified output.
@@ -168,10 +168,10 @@ func writeArgumentType(w io.Writer, t reflect.Type, v interface{}, value bool) {
graphqlType, ok = reflect.Zero(t).Interface().(GraphQLType)
}
if ok {
io.WriteString(w, graphqlType.GetGraphQLType())
_, _ = io.WriteString(w, graphqlType.GetGraphQLType())
if value {
// Value is a required type, so add "!" to the end.
io.WriteString(w, "!")
_, _ = io.WriteString(w, "!")
}
return
}
@@ -186,27 +186,27 @@ func writeArgumentType(w io.Writer, t reflect.Type, v interface{}, value bool) {
switch t.Kind() {
case reflect.Slice, reflect.Array:
// List. E.g., "[Int]".
io.WriteString(w, "[")
_, _ = io.WriteString(w, "[")
writeArgumentType(w, t.Elem(), nil, true)
io.WriteString(w, "]")
_, _ = io.WriteString(w, "]")
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
io.WriteString(w, "Int")
_, _ = io.WriteString(w, "Int")
case reflect.Float32, reflect.Float64:
io.WriteString(w, "Float")
_, _ = io.WriteString(w, "Float")
case reflect.Bool:
io.WriteString(w, "Boolean")
_, _ = io.WriteString(w, "Boolean")
default:
n := t.Name()
if n == "string" {
n = "String"
}
io.WriteString(w, n)
_, _ = io.WriteString(w, n)
}

if value {
// Value is a required type, so add "!" to the end.
io.WriteString(w, "!")
_, _ = io.WriteString(w, "!")
}
}

@@ -241,7 +241,7 @@ func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) error
return nil
}
if !inline {
io.WriteString(w, "{")
_, _ = io.WriteString(w, "{")
}
iter := 0
for i := 0; i < t.NumField(); i++ {
@@ -252,16 +252,16 @@ func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) error
continue
}
if iter != 0 {
io.WriteString(w, ",")
_, _ = io.WriteString(w, ",")
}
iter++

inlineField := f.Anonymous && !ok
if !inlineField {
if ok {
io.WriteString(w, value)
_, _ = io.WriteString(w, value)
} else {
io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase())
_, _ = io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase())
}
}
// Skip writeQuery if the GraphQL type associated with the filed is scalar
@@ -274,7 +274,7 @@ func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) error
}
}
if !inline {
io.WriteString(w, "}")
_, _ = io.WriteString(w, "}")
}
case reflect.Slice:
if t.Elem().Kind() != reflect.Array {
38 changes: 34 additions & 4 deletions subscription.go
Original file line number Diff line number Diff line change
@@ -408,6 +408,11 @@ func (sc *SubscriptionClient) GetContext() context.Context {
return sc.getContext().GetContext()
}

// GetSubscription get the subscription state by id
func (sc *SubscriptionClient) GetSubscription(id string) *Subscription {
return sc.getContext().GetSubscription(id)
}

// WithWebSocket replaces customized websocket client constructor
// In default, subscription client uses https://github.com/nhooyr/websocket
func (sc *SubscriptionClient) WithWebSocket(fn func(sc *SubscriptionClient) (WebsocketConn, error)) *SubscriptionClient {
@@ -742,7 +747,7 @@ func (sc *SubscriptionClient) Run() error {
}

if err := sc.init(); err != nil {
return fmt.Errorf("retry timeout. exiting...")
return fmt.Errorf("retry timeout, %w", err)
}

subContext := sc.getContext()
@@ -885,7 +890,7 @@ func (sc *SubscriptionClient) reset() {
continue
}
if sub.status == SubscriptionRunning {
sc.protocol.Unsubscribe(subContext, sub)
_ = sc.protocol.Unsubscribe(subContext, sub)
}

// should restart subscriptions with new id
@@ -1063,8 +1068,12 @@ func (wh *WebsocketHandler) GetCloseStatus(err error) int32 {
func newWebsocketConn(sc *SubscriptionClient) (WebsocketConn, error) {

options := &websocket.DialOptions{
Subprotocols: sc.protocol.GetSubprotocols(),
HTTPClient: sc.websocketOptions.HTTPClient,
Subprotocols: sc.protocol.GetSubprotocols(),
HTTPClient: sc.websocketOptions.HTTPClient,
HTTPHeader: sc.websocketOptions.HTTPHeader,
Host: sc.websocketOptions.Host,
CompressionMode: sc.websocketOptions.CompressionMode,
CompressionThreshold: sc.websocketOptions.CompressionThreshold,
}

c, _, err := websocket.Dial(sc.GetContext(), sc.GetURL(), options)
@@ -1082,5 +1091,26 @@ func newWebsocketConn(sc *SubscriptionClient) (WebsocketConn, error) {
// WebsocketOptions allows implementation agnostic configuration of the websocket client
type WebsocketOptions struct {
// HTTPClient is used for the connection.
// Its Transport must return writable bodies for WebSocket handshakes.
// http.Transport does beginning with Go 1.12.
HTTPClient *http.Client

// HTTPHeader specifies the HTTP headers included in the handshake request.
HTTPHeader http.Header

// Host optionally overrides the Host HTTP header to send. If empty, the value
// of URL.Host will be used.
Host string

// CompressionMode controls the compression mode.
// Defaults to CompressionDisabled.
//
// See docs on CompressionMode for details.
CompressionMode websocket.CompressionMode

// CompressionThreshold controls the minimum size of a message before compression is applied.
//
// Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes
// for CompressionContextTakeover.
CompressionThreshold int
}
21 changes: 16 additions & 5 deletions subscription_graphql_ws_test.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"log"
"math/rand"
"net/http"
"testing"
"time"
@@ -77,6 +78,16 @@ func waitService(endpoint string, timeoutSecs int) error {
return errors.New("unknown error")
}

func randomID() string {
var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")

b := make([]rune, 16)
for i := range b {
b[i] = letter[rand.Intn(len(letter))]
}
return string(b)
}

func waitHasuraService(timeoutSecs int) error {
return waitService(fmt.Sprintf("%s/healthz", hasuraTestHost), timeoutSecs)
}
@@ -137,7 +148,7 @@ func TestGraphqlWS_Subscription(t *testing.T) {

go func() {
if err := subscriptionClient.Run(); err == nil || err.Error() != "exit" {
(*t).Fatalf("got error: %v, want: exit", err)
t.Errorf("got error: %v, want: exit", err)
}
stop <- true
}()
@@ -236,7 +247,7 @@ func TestGraphqlWS_SubscriptionRerun(t *testing.T) {

go func() {
if err := subscriptionClient.Run(); err != nil {
(*t).Fatalf("got error: %v, want: nil", err)
t.Errorf("got error: %v, want: nil", err)
}
}()

@@ -280,11 +291,11 @@ func TestGraphqlWS_SubscriptionRerun(t *testing.T) {
time.Sleep(2 * time.Second)
go func() {
time.Sleep(2 * time.Second)
subscriptionClient.Unsubscribe(subId1)
_ = subscriptionClient.Unsubscribe(subId1)
}()

if err := subscriptionClient.Run(); err != nil {
(*t).Fatalf("got error: %v, want: nil", err)
t.Fatalf("got error: %v, want: nil", err)
}
}

@@ -351,7 +362,7 @@ func TestGraphQLWS_OnError(t *testing.T) {

go func() {
if err := subscriptionClient.Run(); err == nil || websocket.CloseStatus(err) != 4400 {
(*t).Fatalf("got error: %v, want: 4400", err)
t.Errorf("got error: %v, want: 4400", err)
}
stop <- true
}()
428 changes: 0 additions & 428 deletions subscription_test.go

This file was deleted.

693 changes: 225 additions & 468 deletions subscriptions_transport_ws_test.go

Large diffs are not rendered by default.

0 comments on commit 77f1576

Please sign in to comment.