├── pkg ├── driver │ ├── driver_test.go │ ├── protohttp │ │ ├── header.go │ │ ├── client_test.go │ │ ├── stream.go │ │ ├── subscription_listen.go │ │ ├── secrets.go │ │ ├── server_test.go │ │ ├── authorize.go │ │ ├── field.go │ │ ├── union.go │ │ ├── interface.go │ │ ├── subscription_connection.go │ │ ├── client.go │ │ ├── message_types.go │ │ ├── field_test.go │ │ ├── secrets_test.go │ │ ├── union_test.go │ │ ├── interface_test.go │ │ ├── scalar.go │ │ ├── server.go │ │ └── scalar_test.go │ ├── error.go │ ├── plugin │ │ ├── plugin_posix.go │ │ ├── plugin_windows.go │ │ ├── proc_group_posix.go │ │ ├── proc_group_windows.go │ │ ├── plugin_posix_test.go │ │ ├── grpc_test.go │ │ ├── plugin_windows_test.go │ │ ├── grpc.go │ │ ├── plugin_log.go │ │ └── plugin_log_test.go │ ├── secrets.go │ ├── scalar.go │ ├── authorize.go │ ├── registry.go │ ├── union.go │ ├── subscription_connection.go │ ├── interface.go │ ├── field.go │ ├── registry_test.go │ ├── driver.go │ ├── stream.go │ ├── subscription_listen.go │ └── drivertest │ │ └── mock.go ├── types │ ├── arguments_test.go │ ├── arguments.go │ ├── function.go │ ├── http_request.go │ ├── response_path.go │ ├── type_ref.go │ ├── directive.go │ ├── variable.go │ ├── operation_definition.go │ └── selection.go ├── version │ ├── testdata │ │ └── main.go │ ├── version.go │ └── version_test.go ├── utils │ ├── field.go │ ├── field_test.go │ ├── buf.go │ ├── config_test.go │ ├── testdata │ │ └── config.json │ ├── files.go │ └── config.go ├── parser │ ├── util.go │ ├── root_operation.go │ ├── input_object.go │ ├── enum.go │ ├── type.go │ ├── object.go │ ├── schema.go │ ├── nonscalar.go │ ├── field.go │ ├── interface.go │ └── union.go ├── providers │ └── azure │ │ ├── function │ │ ├── graphql │ │ │ └── function.json │ │ ├── run.js │ │ └── host.json │ │ ├── vars │ │ └── vars.go │ │ ├── project │ │ ├── runtimes │ │ │ ├── common.go │ │ │ └── stuccojs.go │ │ ├── config.go │ │ └── router.go │ │ └── driver │ │ └── driver_test.go ├── vars │ └── vars.go ├── router │ ├── authorize_extension.go │ ├── config_test.go │ ├── router_context_extension.go │ └── router_test.go ├── handlers │ ├── recovery.go │ ├── contextprotocol.go │ ├── graphiql.go │ └── webhook_handler_test.go ├── grpc │ ├── stream.go │ ├── set_secrets_test.go │ ├── union_test.go │ ├── subscription_listen.go │ ├── interface_test.go │ ├── field_test.go │ ├── set_secrets.go │ ├── grpc.go │ ├── authorize.go │ ├── union.go │ ├── field.go │ ├── interface.go │ ├── subscription_connection.go │ ├── scalar_test.go │ └── scalar.go ├── cors │ └── cors.go ├── proto │ ├── prototest │ │ └── set_secrets.go │ └── driver │ │ ├── secrets.go │ │ └── authorize.go └── printer │ └── format.go ├── .gitignore ├── .golangci.yml ├── example ├── README.md ├── package.json ├── client.graphql └── stucco.json ├── docker └── azure │ └── Dockerfile ├── README.md ├── cmd ├── local │ ├── local.go │ └── start.go ├── version.go ├── azure │ ├── azure.go │ └── zip_router.go ├── root.go └── config │ ├── config.go │ └── add.go ├── LICENSE ├── docs └── azure.md ├── main.go ├── go.mod └── .github └── workflows └── main.yml /pkg/driver/driver_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | -------------------------------------------------------------------------------- /pkg/types/arguments_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | node_modules 3 | .vscode 4 | .wwwroot 5 | __debug_bin 6 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/header.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | const ( 4 | contentTypeHeader = "content-type" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/types/arguments.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Arguments is a map of values 4 | type Arguments map[string]interface{} 5 | -------------------------------------------------------------------------------- /pkg/types/function.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Function defines a function name in runtime 4 | type Function struct { 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/driver/error.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | // Error passed between runner and router 4 | type Error struct { 5 | Message string `json:"message,omitempty"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/types/http_request.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // HttpRequest represents http request data. 4 | type HttpRequest struct { 5 | Headers map[string]string `json:"headers,omitempty"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/driver/plugin/plugin_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package plugin 4 | 5 | import "os" 6 | 7 | func isExecutable(fi os.FileInfo) bool { 8 | return fi.Mode()&0111 != 0 9 | } 10 | -------------------------------------------------------------------------------- /pkg/version/testdata/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/graphql-editor/stucco/pkg/version" 7 | ) 8 | 9 | func main() { 10 | fmt.Print(version.Version) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/types/response_path.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // ResponsePath is a node in response path. 4 | type ResponsePath struct { 5 | Prev *ResponsePath `json:"responsePath,omitempty"` 6 | Key interface{} `json:"key"` 7 | } 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-rules: 3 | # Exclude some linters from running on tests files. 4 | - path: _test\.go 5 | linters: 6 | - gocyclo 7 | - errcheck 8 | - dupl 9 | - gosec 10 | 11 | -------------------------------------------------------------------------------- /pkg/utils/field.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // FieldName returns resolver name for field 8 | func FieldName(parent, field string) string { 9 | return strings.Join([]string{parent, field}, ".") 10 | } 11 | -------------------------------------------------------------------------------- /pkg/types/type_ref.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // TypeRef is a reference to a type defined in schema 4 | type TypeRef struct { 5 | Name string `json:"name,omitempty"` 6 | NonNull *TypeRef `json:"nonNull,omitempty"` 7 | List *TypeRef `json:"list,omitempty"` 8 | } 9 | -------------------------------------------------------------------------------- /pkg/parser/util.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/graphql-go/graphql/language/ast" 5 | ) 6 | 7 | func setDescription(d *string, descNode ast.DescribableNode) { 8 | if descNode.GetDescription() != nil { 9 | *d = descNode.GetDescription().Value 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pkg/types/directive.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Directive represents an applied directive with arguments 4 | type Directive struct { 5 | Name string `json:"name"` 6 | Arguments Arguments `json:"arguments,omitempty"` 7 | } 8 | 9 | // Directives is a list of directives 10 | type Directives []Directive 11 | -------------------------------------------------------------------------------- /pkg/driver/secrets.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | type Secrets map[string]string 4 | 5 | type SetSecretsInput struct { 6 | // Secrets is a map of references which driver uses to populate secrets map 7 | Secrets Secrets 8 | } 9 | 10 | type SetSecretsOutput struct { 11 | Error *Error `json:"error,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/utils/field_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/graphql-editor/stucco/pkg/utils" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFieldName(t *testing.T) { 11 | assert.Equal(t, utils.FieldName("SomeType", "someField"), "SomeType.someField") 12 | } 13 | -------------------------------------------------------------------------------- /pkg/providers/azure/function/graphql/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "res" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /pkg/providers/azure/vars/vars.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import ( 4 | global_vars "github.com/graphql-editor/stucco/pkg/vars" 5 | ) 6 | 7 | // Vars meta variables relating to stucco itself 8 | type Vars struct { 9 | global_vars.Vars 10 | AzureFunction string 11 | } 12 | 13 | // DefaultVars c 14 | var DefaultVars = Vars{ 15 | Vars: global_vars.DefaultVars, 16 | } 17 | -------------------------------------------------------------------------------- /pkg/types/variable.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Variable is a name of variable defined by client 4 | type Variable struct { 5 | Name string `json:"name"` 6 | } 7 | 8 | // VariableDefinition client defined variable 9 | type VariableDefinition struct { 10 | Variable Variable `json:"variable"` 11 | DefaultValue interface{} `json:"defaultValue,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/client_test.go: -------------------------------------------------------------------------------- 1 | package protohttp_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/graphql-editor/stucco/pkg/driver" 7 | "github.com/graphql-editor/stucco/pkg/driver/protohttp" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestClientImplementsDriver(t *testing.T) { 12 | assert.Implements(t, (*driver.Driver)(nil), new(protohttp.Client)) 13 | } 14 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | A simple example of backend using stucco and stucco-js plugin 4 | 5 | # Running 6 | 7 | ## Install dependencies 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | ## Start server 14 | 15 | ``` 16 | npm run start 17 | ``` 18 | 19 | ## Try it out 20 | 21 | ``` 22 | curl -X POST -H "Content-Type: application/graphql" --data "@client.graphql" localhost:8080/graphql 23 | ``` 24 | -------------------------------------------------------------------------------- /pkg/driver/plugin/plugin_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package plugin 4 | 5 | import ( 6 | "os" 7 | "strings" 8 | ) 9 | 10 | var extecutableExtensions = []string{ 11 | ".exe", 12 | ".cmd", 13 | ".bat", 14 | } 15 | 16 | func isExecutable(fi os.FileInfo) bool { 17 | for _, ext := range extecutableExtensions { 18 | if strings.HasSuffix(fi.Name(), ext) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/stream.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import "github.com/graphql-editor/stucco/pkg/driver" 4 | 5 | // Stream implements driver.Stream. Currently protocol buffer streaming is not supported 6 | // over HTTP 7 | func (c *Client) Stream(driver.StreamInput) driver.StreamOutput { 8 | return driver.StreamOutput{ 9 | Error: &driver.Error{ 10 | Message: "HTTP transport does not support streaming", 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/providers/azure/project/runtimes/common.go: -------------------------------------------------------------------------------- 1 | package runtimes 2 | 3 | import "io" 4 | 5 | // OsType defines target os in Azure Functions 6 | type OsType uint8 7 | 8 | // List of operating systems supported by azure function 9 | const ( 10 | Linux OsType = iota 11 | Windows 12 | ) 13 | 14 | var commonIgnoreList = []string{"/.*"} 15 | 16 | // File included in function generation 17 | type File struct { 18 | io.Reader 19 | Path string 20 | } 21 | -------------------------------------------------------------------------------- /docker/azure/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 as build 2 | 3 | ENV CGO_ENABLED=0 4 | ADD . /app 5 | WORKDIR /app 6 | RUN go run hack/tasks.go bin/cli/linux/amd64/stucco --version=${VERSION} 7 | 8 | FROM mcr.microsoft.com/azure-functions/dotnet:3.0 9 | 10 | ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true 11 | 12 | COPY --from=build /app/pkg/providers/azure/function /home/site/wwwroot 13 | COPY --from=build /app/bin/cli/linux/amd64/stucco /home/site/wwwroot/stucco 14 | -------------------------------------------------------------------------------- /pkg/driver/plugin/proc_group_posix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package plugin 5 | 6 | import ( 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func createProcGroup(cmd *exec.Cmd) { 12 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 13 | } 14 | 15 | func killTree(cmd *exec.Cmd) error { 16 | pgid, err := syscall.Getpgid(cmd.Process.Pid) 17 | if err == nil { 18 | err = syscall.Kill(-pgid, syscall.SIGKILL) 19 | } 20 | return err 21 | } 22 | -------------------------------------------------------------------------------- /pkg/driver/plugin/proc_group_windows.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | func createProcGroup(cmd *exec.Cmd) {} 11 | 12 | func killTree(cmd *exec.Cmd) error { 13 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 14 | defer cancel() 15 | return exec. 16 | CommandContext(ctx, "taskkill", "/pid", strconv.FormatInt(int64(cmd.Process.Pid), 10), "/T", "/F"). 17 | Run() 18 | } 19 | -------------------------------------------------------------------------------- /pkg/vars/vars.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | // Release contains deployment configs 4 | type Release struct { 5 | Host string 6 | DevVersion string 7 | Version string 8 | } 9 | 10 | // Vars meta variables relating to stucco itself 11 | type Vars struct { 12 | Relase Release 13 | } 14 | 15 | // DefaultVars c 16 | var DefaultVars = Vars{ 17 | Relase: Release{ 18 | Host: "stucco-release.fra1.cdn.digitaloceanspaces.com", 19 | DevVersion: "latest", 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /pkg/types/operation_definition.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // OperationDefinition represents operation defined in user schema 4 | type OperationDefinition struct { 5 | Operation string `json:"operation"` 6 | Name string `json:"name"` 7 | VariableDefinitions []VariableDefinition `json:"variableDefinitions,omitempty"` 8 | Directives Directives `json:"directives,omitempty"` 9 | SelectionSet Selections `json:"selectionSet,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/parser/root_operation.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/graphql-go/graphql" 7 | 8 | "github.com/graphql-go/graphql/language/ast" 9 | ) 10 | 11 | type rootOperation ast.OperationTypeDefinition 12 | 13 | func (r rootOperation) config(p *Parser) (o *graphql.Object, err error) { 14 | switch v := p.definitions[r.Type.Name.Value].(type) { 15 | case *ast.ObjectDefinition: 16 | return objectDefintion(p, v) 17 | default: 18 | err = errors.New("root operation must be an object") 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /pkg/utils/buf.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // CopyToReader returns new bytes.Reader with a copy of data read by r 9 | func CopyToReader(r io.Reader) (br *bytes.Reader, err error) { 10 | var buf bytes.Buffer 11 | _, err = io.Copy(&buf, r) 12 | if err == nil { 13 | br = bytes.NewReader(buf.Bytes()) 14 | } 15 | return 16 | } 17 | 18 | // ReaderAtToReader returns a reader backed with ReaderAt 19 | func ReaderAtToReader(r io.ReaderAt, size int64) io.Reader { 20 | return io.NewSectionReader(r, 0, size) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/subscription_listen.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import "github.com/graphql-editor/stucco/pkg/driver" 4 | 5 | // SubscriptionListen implements driver.SubscriptionListen. Currently protocol buffer subscription listening is not supported 6 | // over HTTP 7 | func (c *Client) SubscriptionListen(driver.SubscriptionListenInput) driver.SubscriptionListenOutput { 8 | return driver.SubscriptionListenOutput{ 9 | Error: &driver.Error{ 10 | Message: "HTTP transport does not subscription listening. Try using external subscription", 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "regexp" 5 | "time" 6 | ) 7 | 8 | // BuildVersion is set on compile time representing build version 9 | // If empty, build version was not set 10 | var ( 11 | BuildVersion string 12 | BuildDate = time.Now().UTC().Format("200601021504") 13 | re = regexp.MustCompile(`^v?[0-9]*\.[0-9]*\.[0-9]*$`) 14 | Version = func() string { 15 | v := BuildVersion 16 | if v == "" { 17 | v = BuildDate 18 | } 19 | if !re.Match([]byte(v)) { 20 | v = "dev-" + v 21 | } 22 | return v 23 | }() 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/driver/scalar.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | type ScalarParseInput struct { 6 | Function types.Function `json:"function"` 7 | Value interface{} `json:"value"` 8 | } 9 | type ScalarParseOutput struct { 10 | Response interface{} `json:"response,omitempty"` 11 | Error *Error `json:"error,omitempty"` 12 | } 13 | 14 | type ScalarSerializeInput struct { 15 | Function types.Function `json:"function"` 16 | Value interface{} `json:"value"` 17 | } 18 | type ScalarSerializeOutput struct { 19 | Response interface{} `json:"response,omitempty"` 20 | Error *Error `json:"error,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stucco-example", 3 | "version": "1.0.0", 4 | "description": "Example of using stucco with NodeJS", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "stucco" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/graphql-editor/stucco.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/graphql-editor/stucco/issues" 18 | }, 19 | "homepage": "https://github.com/graphql-editor/stucco#readme", 20 | "dependencies": { 21 | "stucco-js": "^0.10.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | GraphQL server runner created with serverless in mind. Project is in early alpha phase. Backwards compatibility is not guaranteed. 4 | 5 | # Installation 6 | 7 | * macOS/Linux 8 | ``` 9 | $ curl https://stucco-release.fra1.cdn.digitaloceanspaces.com/latest/$(uname | tr '[:upper:]' '[:lower:]')/$(uname -m | sed 's/^x86_64$/amd64/g)/stucco 10 | ``` 11 | 12 | * Windows 13 | [Download for 64-bit](https://stucco-release.fra1.cdn.digitaloceanspaces.com/latest/windows/amd64/stucco.exe) 14 | # Drivers 15 | 16 | To add a new provider/runtime implement `driver.Driver` 17 | 18 | # Example 19 | 20 | An example project using `stucco-js` driver is available in example. 21 | -------------------------------------------------------------------------------- /pkg/providers/azure/function/run.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process') 2 | const { platform, arch } = require('os') 3 | const { join } = require('path') 4 | const hostos = platform() === 'win32' ? 'windows' : platform() 5 | const archMapping = { 6 | x64: 'amd64', 7 | x32: '386', 8 | ia32: '386', 9 | } 10 | const hostarch = archMapping[arch()] !== undefined ? archMapping[arch()] : arch() 11 | const ext = hostos === 'windows' ? '.exe' : '' 12 | const args = ['azure', 'start'] 13 | const stucco = spawn(join('stucco', hostos, hostarch, 'stucco' + ext), args.concat(process.argv.slice(2))) 14 | process.stdin.pipe(stucco.stdin) 15 | stucco.stdout.pipe(process.stdout) 16 | stucco.stderr.pipe(process.stderr) 17 | -------------------------------------------------------------------------------- /example/client.graphql: -------------------------------------------------------------------------------- 1 | { 2 | character(id: "r2_d2") { 3 | id 4 | name 5 | friends{ 6 | name 7 | } 8 | friendsConnection { 9 | friends { 10 | name 11 | } 12 | pageInfo { 13 | startCursor 14 | endCursor 15 | hasNextPage 16 | } 17 | } 18 | appearsIn 19 | ... on Human { 20 | starships { 21 | name 22 | } 23 | } 24 | ... on Droid { 25 | primaryFunction 26 | } 27 | } 28 | search(text:"X-Wing") { 29 | __typename 30 | ... on Starship { 31 | name 32 | length 33 | } 34 | ... on Human { 35 | name 36 | } 37 | ... on Droid { 38 | name 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/providers/azure/project/config.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/graphql-editor/stucco/pkg/server" 7 | ) 8 | 9 | type AzureOpts struct { 10 | Webhooks []string `json:"webhooks"` 11 | } 12 | 13 | // Config represents azure server config 14 | type Config struct { 15 | server.Config 16 | AzureOpts AzureOpts `json:"azureOpts"` 17 | } 18 | 19 | func (c *Config) UnmarshalJSON(data []byte) error { 20 | if err := json.Unmarshal(data, &c.Config); err != nil { 21 | return err 22 | } 23 | opts := struct { 24 | AzureOpts AzureOpts `json:"azureOpts"` 25 | }{} 26 | err := json.Unmarshal(data, &opts) 27 | if err == nil { 28 | c.AzureOpts = opts.AzureOpts 29 | } 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /pkg/providers/azure/function/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[3.*, 4.0.0)" 14 | }, 15 | "extensions": { 16 | "http": { 17 | "routePrefix": "" 18 | } 19 | }, 20 | "customHandler": { 21 | "description": { 22 | "defaultExecutablePath": "node", 23 | "workingDirectory": "", 24 | "arguments": ["run.js", "--cert", "./cert.pem", "--key", "./key.pem"] 25 | }, 26 | "enableForwardingHttpRequest": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/driver/authorize.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | // AuthorizeInput represents data passed to authorize function 6 | type AuthorizeInput struct { 7 | Function types.Function `json:"function,omitempty"` 8 | Query string `json:"query,omitempty"` 9 | OperationName string `json:"operationName,omitempty"` 10 | VariableValues map[string]interface{} `json:"variableValues,omitempty"` 11 | Protocol interface{} `json:"protocol,omitempty"` 12 | } 13 | 14 | // AuthorizeOutput is an authorize response 15 | type AuthorizeOutput struct { 16 | Response bool `json:"response,omitempty"` 17 | Error *Error `json:"error,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /pkg/driver/registry.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Config defines a config that a driver satisfies. 8 | // Only on driver per config can be definied in registry 9 | type Config struct { 10 | Provider string `json:"provider,omitempty"` 11 | Runtime string `json:"runtime,omitempty"` 12 | } 13 | 14 | var ( 15 | lock = sync.Mutex{} 16 | drivers = map[Config]Driver{} 17 | ) 18 | 19 | // Register adds a new driver for a user config 20 | func Register(c Config, d Driver) { 21 | lock.Lock() 22 | drivers[c] = d 23 | lock.Unlock() 24 | } 25 | 26 | // GetDriver returns a driver matching user config for a runner 27 | func GetDriver(c Config) Driver { 28 | lock.Lock() 29 | d := drivers[c] 30 | lock.Unlock() 31 | return d 32 | } 33 | -------------------------------------------------------------------------------- /pkg/router/authorize_extension.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/graphql-go/graphql" 8 | ) 9 | 10 | type authorizeExtension struct { 11 | baseExtension 12 | authorizeHandler func(*graphql.Params) (bool, error) 13 | } 14 | 15 | func (a authorizeExtension) Init(ctx context.Context, p *graphql.Params) context.Context { 16 | if a.authorizeHandler != nil { 17 | rtContext := getRouterContext(ctx) 18 | if rtContext.Error == nil { 19 | ok, err := a.authorizeHandler(p) 20 | if err != nil || !ok { 21 | if err == nil { 22 | err = errors.New("unauthorized") 23 | } 24 | rtContext.Error = err 25 | } 26 | } 27 | } 28 | return ctx 29 | } 30 | func (a authorizeExtension) Name() string { return "authorize extension" } 31 | -------------------------------------------------------------------------------- /pkg/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version_test 2 | 3 | import ( 4 | "os/exec" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestVersions(t *testing.T) { 11 | o, err := exec.Command("go", "run", "testdata/main.go").Output() 12 | assert.NoError(t, err) 13 | assert.Regexp(t, "^dev-[0-9]{12}$", string(o)) 14 | o, err = exec.Command("go", "run", "-ldflags=-X github.com/graphql-editor/stucco/pkg/version.BuildVersion=v1.0.0", "testdata/main.go").Output() 15 | assert.NoError(t, err) 16 | assert.Equal(t, "v1.0.0", string(o)) 17 | o, err = exec.Command("go", "run", "-ldflags=-X github.com/graphql-editor/stucco/pkg/version.BuildVersion=123456abcdef", "testdata/main.go").Output() 18 | assert.NoError(t, err) 19 | assert.Equal(t, "dev-123456abcdef", string(o)) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/parser/input_object.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/graphql-go/graphql" 5 | "github.com/graphql-go/graphql/language/ast" 6 | ) 7 | 8 | func inputObjectDefinition(p *Parser, i *ast.InputObjectDefinition) (t *graphql.InputObject, err error) { 9 | iCfg := graphql.InputObjectConfig{ 10 | Name: i.Name.Value, 11 | } 12 | setDescription(&iCfg.Description, i) 13 | t = graphql.NewInputObject(iCfg) 14 | p.gqlTypeMap[t.Name()] = t 15 | fields := graphql.InputObjectConfigFieldMap{} 16 | for _, f := range i.Fields { 17 | var field *graphql.InputObjectFieldConfig 18 | field, err = makeInputObjectField(p, f) 19 | if err != nil { 20 | return 21 | } 22 | fields[f.Name.Value] = field 23 | } 24 | iCfg.Fields = fields 25 | *t = *graphql.NewInputObject(iCfg) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /pkg/driver/union.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | type UnionResolveTypeInfo struct { 6 | FieldName string `json:"fieldName"` 7 | Path *types.ResponsePath `json:"path,omitempty"` 8 | ReturnType *types.TypeRef `json:"returnType,omitempty"` 9 | ParentType *types.TypeRef `json:"parentType,omitempty"` 10 | Operation *types.OperationDefinition `json:"operation,omitempty"` 11 | VariableValues map[string]interface{} `json:"variableValues,omitempty"` 12 | } 13 | 14 | type UnionResolveTypeInput struct { 15 | Function types.Function 16 | Value interface{} 17 | Info UnionResolveTypeInfo 18 | } 19 | type UnionResolveTypeOutput struct { 20 | Type types.TypeRef 21 | Error *Error 22 | } 23 | -------------------------------------------------------------------------------- /pkg/parser/enum.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/graphql-go/graphql" 5 | "github.com/graphql-go/graphql/language/ast" 6 | ) 7 | 8 | func enumDefinition(p *Parser, e *ast.EnumDefinition) (t *graphql.Enum, err error) { 9 | eCfg := graphql.EnumConfig{ 10 | Name: e.Name.Value, 11 | } 12 | setDescription(&eCfg.Description, e) 13 | t = graphql.NewEnum(eCfg) 14 | p.gqlTypeMap[t.Name()] = t 15 | for _, v := range e.Values { 16 | if eCfg.Values == nil { 17 | eCfg.Values = make(graphql.EnumValueConfigMap, len(e.GetVariableDefinitions())) 18 | } 19 | eCfg.Values[v.Name.Value] = &graphql.EnumValueConfig{ 20 | Value: v.Name.Value, 21 | } 22 | if v.Description != nil { 23 | eCfg.Values[v.Name.Value].Description = v.Description.Value 24 | } 25 | } 26 | *t = *graphql.NewEnum(eCfg) 27 | return t, nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/driver/subscription_connection.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | // SubscriptionConnectionInput represents input to a function which creates subscription connection data 6 | type SubscriptionConnectionInput struct { 7 | Function types.Function 8 | Query string `json:"query,omitempty"` 9 | VariableValues map[string]interface{} `json:"variableValues,omitempty"` 10 | OperationName string `json:"operationName,omitempty"` 11 | Protocol interface{} `json:"protocol,omitempty"` 12 | Operation *types.OperationDefinition `json:"operation,omitempty"` 13 | } 14 | 15 | // SubscriptionConnectionOutput represents response from a function which creates subscription connection data 16 | type SubscriptionConnectionOutput struct { 17 | Response interface{} `json:"response,omitempty"` 18 | Error *Error `json:"error,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /pkg/types/selection.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // FragmentDefinition is a fragment definition from client schema 4 | type FragmentDefinition struct { 5 | Directives Directives `json:"directives,omitempty"` 6 | TypeCondition TypeRef `json:"typeCondition"` 7 | SelectionSet Selections `json:"selectionSet"` 8 | VariableDefinitions []VariableDefinition `json:"variableDefinitions,omitempty"` 9 | } 10 | 11 | // Selection is a represents a field or fragment requested by client 12 | type Selection struct { 13 | Name string `json:"name,omitempty"` 14 | Arguments Arguments `json:"arguments,omitempty"` 15 | Directives Directives `json:"directives,omitempty"` 16 | SelectionSet Selections `json:"selectionSet,omitempty"` 17 | Definition *FragmentDefinition `json:"definition,omitempty"` 18 | } 19 | 20 | // Selections list of selections 21 | type Selections []Selection 22 | -------------------------------------------------------------------------------- /cmd/local/local.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 NAME HERE 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package localcmd 17 | 18 | import ( 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | // NewLocalCommand create new local command 23 | func NewLocalCommand() *cobra.Command { 24 | localCommand := &cobra.Command{ 25 | Use: "local", 26 | Short: "Local stucco runner", 27 | } 28 | localCommand.AddCommand(NewStartCommand()) 29 | return localCommand 30 | } 31 | -------------------------------------------------------------------------------- /pkg/handlers/recovery.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "runtime/debug" 6 | ) 7 | 8 | // ErrorLog used by recovery handler 9 | type ErrorLog interface { 10 | Errorf(string, ...interface{}) 11 | } 12 | 13 | // RecoveryHandler recovers from panics to return Internal Server Error http response 14 | func RecoveryHandler(next http.Handler, logger ErrorLog) http.Handler { 15 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 16 | defer func() { 17 | err := recover() 18 | if err != nil { 19 | if logger != nil { 20 | logger.Errorf("%v\n", err) 21 | logger.Errorf("%s\n", debug.Stack()) 22 | } 23 | rw.Header().Set("Content-Type", "text/plain") 24 | rw.WriteHeader(http.StatusInternalServerError) 25 | if _, err := rw.Write( 26 | []byte("There was an internal server error"), 27 | ); err != nil && logger != nil { 28 | logger.Errorf("%v\n", err) 29 | } 30 | } 31 | }() 32 | next.ServeHTTP(rw, r) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 NAME HERE 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/graphql-editor/stucco/pkg/version" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // NewVersionCommand represents the version command 26 | func NewVersionCommand() *cobra.Command { 27 | versionCommand := &cobra.Command{ 28 | Use: "version", 29 | Short: "Return stucco version", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | fmt.Println(version.Version) 32 | }, 33 | } 34 | return versionCommand 35 | } 36 | -------------------------------------------------------------------------------- /cmd/azure/azure.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 NAME HERE 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package azurecmd 17 | 18 | import ( 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | // NewAzureCommand represents the azure command 23 | func NewAzureCommand() *cobra.Command { 24 | var azureCmd = &cobra.Command{ 25 | Use: "azure", 26 | Short: "Set of tools to help with Azure Functions and stucco integration", 27 | } 28 | azureCmd.AddCommand(NewStartCommand()) 29 | azureCmd.AddCommand(NewZipRouterCommand()) 30 | azureCmd.AddCommand(NewZipFunctionCommand()) 31 | return azureCmd 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2020 Kamil Matysiewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Kamil Matysiewicz 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | -------------------------------------------------------------------------------- /pkg/driver/interface.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | // InterfaceResolveTypeInfo contains information about current state of query 6 | // for interface type resolution 7 | type InterfaceResolveTypeInfo struct { 8 | FieldName string `json:"fieldName"` 9 | Path *types.ResponsePath `json:"path,omitempty"` 10 | ReturnType *types.TypeRef `json:"returnType,omitempty"` 11 | ParentType *types.TypeRef `json:"parentType,omitempty"` 12 | Operation *types.OperationDefinition `json:"operation,omitempty"` 13 | VariableValues map[string]interface{} `json:"variableValues,omitempty"` 14 | } 15 | 16 | // InterfaceResolveTypeInput represents a request of interface type resolution for 17 | // GraphQL query 18 | type InterfaceResolveTypeInput struct { 19 | Function types.Function 20 | Value interface{} 21 | Info InterfaceResolveTypeInfo 22 | } 23 | 24 | // InterfaceResolveTypeOutput represents an output returned by runner for request of 25 | // interface type resolution 26 | type InterfaceResolveTypeOutput struct { 27 | Type types.TypeRef 28 | Error *Error 29 | } 30 | -------------------------------------------------------------------------------- /pkg/parser/type.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/graphql-go/graphql" 7 | 8 | "github.com/graphql-go/graphql/language/ast" 9 | ) 10 | 11 | func toGraphQLType(p *Parser, tt ast.Type) (gt graphql.Type, err error) { 12 | switch t := tt.(type) { 13 | case *ast.Named: 14 | n := t.Name.Value 15 | switch n { 16 | case graphql.Int.Name(): 17 | gt = graphql.Int 18 | case graphql.Float.Name(): 19 | gt = graphql.Float 20 | case graphql.String.Name(): 21 | gt = graphql.String 22 | case graphql.Boolean.Name(): 23 | gt = graphql.Boolean 24 | case graphql.ID.Name(): 25 | gt = graphql.ID 26 | default: 27 | d, ok := p.definitions[n] 28 | if !ok { 29 | err = errors.New("undefined type " + n) 30 | break 31 | } 32 | gt, err = customDefinition(p, d) 33 | } 34 | case *ast.NonNull: 35 | gt, err = toGraphQLType(p, t.Type) 36 | if err != nil { 37 | break 38 | } 39 | gt = graphql.NewNonNull(gt) 40 | case *ast.List: 41 | gt, err = toGraphQLType(p, t.Type) 42 | if err != nil { 43 | break 44 | } 45 | gt = graphql.NewList(gt) 46 | default: 47 | err = errors.New("type not supported") 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /pkg/router/config_test.go: -------------------------------------------------------------------------------- 1 | package router_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/graphql-editor/stucco/pkg/router" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestEnvironmentMerge(t *testing.T) { 11 | data := []struct { 12 | title string 13 | in router.Environment 14 | merge router.Environment 15 | expected router.Environment 16 | }{ 17 | { 18 | title: "OriginalStays", 19 | in: router.Environment{ 20 | Provider: "provider1", 21 | Runtime: "runtime1", 22 | }, 23 | merge: router.Environment{ 24 | Provider: "provider2", 25 | Runtime: "runtime2", 26 | }, 27 | expected: router.Environment{ 28 | Provider: "provider1", 29 | Runtime: "runtime1", 30 | }, 31 | }, 32 | { 33 | title: "Overrides", 34 | in: router.Environment{ 35 | Provider: "", 36 | Runtime: "", 37 | }, 38 | merge: router.Environment{ 39 | Provider: "provider2", 40 | Runtime: "runtime2", 41 | }, 42 | expected: router.Environment{ 43 | Provider: "provider2", 44 | Runtime: "runtime2", 45 | }, 46 | }, 47 | } 48 | for i := range data { 49 | tt := data[i] 50 | t.Run(tt.title, func(t *testing.T) { 51 | tt.in.Merge(tt.merge) 52 | assert.Equal(t, tt.expected, tt.in) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/handlers/contextprotocol.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/router" 8 | ) 9 | 10 | func protocolFromRequest(r *http.Request) map[string]interface{} { 11 | headers := r.Header.Clone() 12 | if headers.Get("x-forwarded-proto") == "" { 13 | if r.TLS != nil { 14 | headers.Set("x-forwarded-proto", "https") 15 | } 16 | } 17 | return map[string]interface{}{ 18 | "method": r.Method, 19 | "headers": r.Header.Clone(), 20 | "host": r.Host, 21 | "remoteAddress": r.RemoteAddr, 22 | "proto": r.Proto, 23 | "url": map[string]interface{}{ 24 | "query": r.URL.RawQuery, 25 | "path": r.URL.Path, 26 | "host": r.URL.Host, 27 | "scheme": r.URL.Scheme, 28 | }, 29 | } 30 | } 31 | 32 | // WithProtocolInContext appends request headers to context object 33 | func WithProtocolInContext(next http.Handler) http.Handler { 34 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 35 | rawSub := r.URL.Query().Get("raw_subscription") 36 | next.ServeHTTP( 37 | rw, 38 | r.WithContext( 39 | context.WithValue( 40 | context.WithValue(r.Context(), router.ProtocolKey, protocolFromRequest(r)), 41 | router.RawSubscriptionKey, 42 | rawSub == "true", 43 | ), 44 | ), 45 | ) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/parser/object.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/graphql-go/graphql" 7 | "github.com/graphql-go/graphql/language/ast" 8 | ) 9 | 10 | func objectDefintion(p *Parser, o *ast.ObjectDefinition) (t *graphql.Object, err error) { 11 | oCfg := graphql.ObjectConfig{ 12 | Name: o.Name.Value, 13 | } 14 | setDescription(&oCfg.Description, o) 15 | t = graphql.NewObject(oCfg) 16 | p.gqlTypeMap[t.Name()] = t 17 | fields := graphql.Fields{} 18 | for _, f := range o.Fields { 19 | var field *graphql.Field 20 | field, err = makeField(p, o.Name.Value, f) 21 | if err != nil { 22 | return 23 | } 24 | fields[f.Name.Value] = field 25 | } 26 | var interfaces []*graphql.Interface 27 | for _, definition := range p.definitions { 28 | if idef, ok := definition.(*ast.InterfaceDefinition); ok { 29 | for _, iface := range o.Interfaces { 30 | if idef.Name.Value == iface.Name.Value { 31 | it, err := customDefinition(p, idef) 32 | if err != nil { 33 | return nil, err 34 | } 35 | gqlIface, ok := it.(*graphql.Interface) 36 | if !ok { 37 | return nil, errors.New("object can only implement interface") 38 | } 39 | interfaces = append(interfaces, gqlIface) 40 | } 41 | } 42 | } 43 | } 44 | oCfg.Fields = fields 45 | oCfg.Interfaces = interfaces 46 | *t = *graphql.NewObject(oCfg) 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /pkg/driver/field.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | // FieldResolveInfo defines information about current field resolution 6 | type FieldResolveInfo struct { 7 | FieldName string `json:"fieldName"` 8 | Path *types.ResponsePath `json:"path,omitempty"` 9 | ReturnType *types.TypeRef `json:"returnType,omitempty"` 10 | ParentType *types.TypeRef `json:"parentType,omitempty"` 11 | Operation *types.OperationDefinition `json:"operation,omitempty"` 12 | VariableValues map[string]interface{} `json:"variableValues,omitempty"` 13 | RootValue interface{} `json:"rootValue,omitempty"` 14 | } 15 | 16 | // FieldResolveInput represents data passed to field resolution 17 | type FieldResolveInput struct { 18 | Function types.Function 19 | Source interface{} `json:"source,omitempty"` 20 | Arguments types.Arguments `json:"arguments,omitempty"` 21 | Info FieldResolveInfo `json:"info"` 22 | Protocol interface{} `json:"protocol,omitempty"` 23 | SubscriptionPayload interface{} `json:"subscriptionPayload,omitempty"` 24 | } 25 | 26 | // FieldResolveOutput is a result of a field resolution 27 | type FieldResolveOutput struct { 28 | Response interface{} `json:"response,omitempty"` 29 | Error *Error `json:"error,omitempty"` 30 | } 31 | -------------------------------------------------------------------------------- /pkg/grpc/stream.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/graphql-editor/stucco/pkg/driver" 5 | protoDriverService "github.com/graphql-editor/stucco_proto/go/driver_service" 6 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 7 | ) 8 | 9 | // Stream TODO: client side stream requests 10 | func (m *Client) Stream(input driver.StreamInput) (s driver.StreamOutput) { 11 | return driver.StreamOutput{ 12 | Error: &driver.Error{ 13 | Message: "Streaming not yet implemented", 14 | }, 15 | } 16 | } 17 | 18 | // StreamHandler interface must be implemented by user to handle stream requests from subscriptions 19 | type StreamHandler interface { 20 | // Handle handles subscription streaming requests 21 | Handle(*protoMessages.StreamRequest, protoDriverService.Driver_StreamServer) error 22 | } 23 | 24 | // StreamHandlerFunc is a convienience wrapper implementing StreamHandler interface 25 | type StreamHandlerFunc func(*protoMessages.StreamRequest, protoDriverService.Driver_StreamServer) error 26 | 27 | // Handle implements StreamHandler.Handle method 28 | func (f StreamHandlerFunc) Handle(s *protoMessages.StreamRequest, ss protoDriverService.Driver_StreamServer) error { 29 | return f(s, ss) 30 | } 31 | 32 | // Stream hands over stream request to user defined handler for stream 33 | func (m *Server) Stream(s *protoMessages.StreamRequest, ss protoDriverService.Driver_StreamServer) error { 34 | return m.StreamHandler.Handle(s, ss) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/driver/registry_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/graphql-editor/stucco/pkg/driver" 7 | "github.com/graphql-editor/stucco/pkg/driver/drivertest" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRegistry(t *testing.T) { 12 | data := []struct { 13 | title string 14 | registerConfig driver.Config 15 | registerDriver driver.Driver 16 | getDriverConfig driver.Config 17 | getDriverDriver driver.Driver 18 | }{ 19 | { 20 | title: "ReturnsRegisteredDriver", 21 | registerConfig: driver.Config{ 22 | Provider: "provider", 23 | Runtime: "runtime", 24 | }, 25 | registerDriver: new(drivertest.MockDriver), 26 | getDriverConfig: driver.Config{ 27 | Provider: "provider", 28 | Runtime: "runtime", 29 | }, 30 | getDriverDriver: new(drivertest.MockDriver), 31 | }, 32 | { 33 | title: "ReturnsNilOnNotFound", 34 | registerConfig: driver.Config{ 35 | Provider: "provider", 36 | Runtime: "runtime", 37 | }, 38 | registerDriver: new(drivertest.MockDriver), 39 | getDriverConfig: driver.Config{ 40 | Provider: "provider2", 41 | Runtime: "runtime", 42 | }, 43 | }, 44 | } 45 | for i := range data { 46 | tt := data[i] 47 | t.Run(tt.title, func(t *testing.T) { 48 | driver.Register(tt.registerConfig, tt.registerDriver) 49 | assert.Equal(t, tt.getDriverDriver, driver.GetDriver(tt.getDriverConfig)) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/azure.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Stucco router that can be run by Azure Functions host. 4 | 5 | # Usage 6 | 7 | ## Run locally 8 | 9 | Currently router can only be ran locally on Linux and macOS because it depends on [azure-functions-golang-worker](https://github.com/graphql-editor/azure-functions-golang-worker/) which does not support running plugin functions on Windows. 10 | 11 | ### Dependencies 12 | * [azure-functions-core-tools@v3](https://github.com/Azure/azure-functions-core-tools) 13 | * [azure-functions-golang-worker](https://github.com/graphql-editor/azure-functions-golang-worker/) 14 | 15 | ### Run 16 | 17 | ``` 18 | $ STUCCO_SCHEMA=path/to/schema.graphql STUCCO_CONFIG=path/to/stucco.json STUCCO_WORKER_BASE_URL=http://worker.url func start 19 | ``` 20 | 21 | ## Docker 22 | 23 | ### New image 24 | 25 | To create new router image just add schema.graphql and stucco.json to base image 26 | 27 | ``` 28 | FROM gqleditor/stucco-router-azure-worker:latest 29 | 30 | COPY schema.graphql /home/site/wwwroot/schema.graphql 31 | COPY stucco.json /home/site/wwwroot/stucco.json 32 | ``` 33 | 34 | ### Run using base image 35 | 36 | ``` 37 | $ docker run -p 8080:80 -e STUCCO_SCHEMA=path/to/schema.graphql -e STUCCO_CONFIG=path/to/stucco.json -e STUCCO_WORKER_BASE_URL=http://worker.url gqleditor/stucco-router-azure-worker:latest 38 | ``` 39 | 40 | # Notes 41 | 42 | By default function.json has `function` auth level which makes them inaccessible locally. Edit `authLevel` field in graphql/function.json to make it debuggable locally. 43 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/secrets.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // SetSecrets using http 13 | func (c *Client) SetSecrets(input driver.SetSecretsInput) driver.SetSecretsOutput { 14 | var out driver.SetSecretsOutput 15 | var body bytes.Buffer 16 | err := protodriver.WriteSetSecretsInput(&body, input) 17 | if err == nil { 18 | var b []byte 19 | if b, err = c.do(message{ 20 | contentType: setSecretsRequestMessage, 21 | responseContentType: setSecretsResponseMessage, 22 | b: body.Bytes(), 23 | }); err == nil { 24 | out, err = protodriver.ReadSetSecretsOutput(bytes.NewReader(b)) 25 | } 26 | } 27 | if err != nil { 28 | out.Error = &driver.Error{ 29 | Message: err.Error(), 30 | } 31 | } 32 | return out 33 | } 34 | 35 | func (h *Handler) setSecrets(req *http.Request, rw http.ResponseWriter) error { 36 | rw.Header().Add(contentTypeHeader, setSecretsResponseMessage.String()) 37 | in, err := protodriver.ReadSetSecretsInput(req.Body) 38 | if err == nil { 39 | req.Body.Close() 40 | err = protodriver.WriteFieldResolveOutput(rw, h.SetSecrets(in)) 41 | } 42 | if err != nil { 43 | err = writeProto(rw, &protoMessages.SetSecretsResponse{ 44 | Error: &protoMessages.Error{ 45 | Msg: err.Error(), 46 | }, 47 | }) 48 | } 49 | return err 50 | } 51 | -------------------------------------------------------------------------------- /pkg/grpc/set_secrets_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/graphql-editor/stucco/pkg/grpc" 8 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func TestClientSetSecrets(t *testing.T) { 14 | prototest.RunSetSecretsClientTests(t, func(t *testing.T, tt prototest.SetSecretsClientTest) { 15 | driverClientMock := new(driverClientMock) 16 | driverClientMock.On( 17 | "SetSecrets", 18 | mock.Anything, 19 | tt.ProtoRequest, 20 | ).Return(tt.ProtoResponse, tt.ProtoError) 21 | client := grpc.Client{ 22 | Client: driverClientMock, 23 | } 24 | out := client.SetSecrets(tt.Input) 25 | assert.Equal(t, tt.Expected, out) 26 | }) 27 | } 28 | 29 | func TestServerSetSecrets(t *testing.T) { 30 | prototest.RunSetSecretsServerTests(t, func(t *testing.T, tt prototest.SetSecretsServerTest) { 31 | setSecretsMock := new(setSecretsMock) 32 | setSecretsMock.On("Handle", tt.HandlerInput).Return(tt.HandlerOutput) 33 | srv := grpc.Server{ 34 | SetSecretsHandler: setSecretsMock, 35 | } 36 | out, err := srv.SetSecrets(context.Background(), tt.Input) 37 | assert.NoError(t, err) 38 | assert.Equal(t, tt.Expected, out) 39 | }) 40 | t.Run("RecoversFromPanic", func(t *testing.T) { 41 | srv := grpc.Server{} 42 | out, err := srv.SetSecrets(context.Background(), nil) 43 | assert.NoError(t, err) 44 | assert.NotNil(t, out.Error) 45 | assert.NotEmpty(t, out.Error.Msg) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/utils/config_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/graphql-editor/stucco/pkg/router" 12 | "github.com/graphql-editor/stucco/pkg/utils" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | var expectedConfig = func() router.Config { 17 | var cfg router.Config 18 | b, err := ioutil.ReadFile("./testdata/config.json") 19 | if err == nil { 20 | err = json.Unmarshal(b, &cfg) 21 | } 22 | if err != nil { 23 | panic(err) 24 | } 25 | return cfg 26 | } 27 | 28 | func TestLoadFileFromFileSystem(t *testing.T) { 29 | var cfg router.Config 30 | assert.NoError(t, utils.LoadConfigFile("./testdata/config", &cfg)) 31 | assert.Equal(t, expectedConfig(), cfg) 32 | assert.Error(t, utils.LoadConfigFile("./testdata/invalid", &cfg)) 33 | } 34 | 35 | func TestLoadFileFromRemote(t *testing.T) { 36 | srv := http.Server{ 37 | Handler: http.FileServer(http.Dir("./testdata")), 38 | } 39 | l, err := net.Listen("tcp", ":8080") 40 | if err != nil { 41 | panic(err) 42 | } 43 | go srv.Serve(l) 44 | defer srv.Shutdown(context.Background()) 45 | var cfg router.Config 46 | assert.NoError(t, utils.LoadConfigFile("http://localhost:8080/config.json", &cfg)) 47 | assert.Equal(t, expectedConfig(), cfg) 48 | assert.Error(t, utils.LoadConfigFile("http://localhost/config", &cfg)) 49 | assert.NoError(t, utils.LoadConfigFile("http://localhost:8080/config.json?some=arg&in=url", &cfg)) 50 | assert.Equal(t, expectedConfig(), cfg) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/server_test.go: -------------------------------------------------------------------------------- 1 | package protohttp_test 2 | 3 | import ( 4 | "github.com/graphql-editor/stucco/pkg/driver" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type mockMuxer struct { 9 | mock.Mock 10 | } 11 | 12 | func (m *mockMuxer) Authorize(in driver.AuthorizeInput) (bool, error) { 13 | called := m.Called(in) 14 | return called.Bool(0), called.Error(1) 15 | } 16 | 17 | func (m *mockMuxer) FieldResolve(in driver.FieldResolveInput) (interface{}, error) { 18 | called := m.Called(in) 19 | return called.Get(0), called.Error(1) 20 | } 21 | 22 | func (m *mockMuxer) InterfaceResolveType(in driver.InterfaceResolveTypeInput) (string, error) { 23 | called := m.Called(in) 24 | return called.String(0), called.Error(1) 25 | } 26 | 27 | func (m *mockMuxer) SetSecrets(in driver.SetSecretsInput) error { 28 | return m.Called(in).Error(0) 29 | } 30 | 31 | func (m *mockMuxer) ScalarParse(in driver.ScalarParseInput) (interface{}, error) { 32 | called := m.Called(in) 33 | return called.Get(0), called.Error(1) 34 | } 35 | 36 | func (m *mockMuxer) ScalarSerialize(in driver.ScalarSerializeInput) (interface{}, error) { 37 | called := m.Called(in) 38 | return called.Get(0), called.Error(1) 39 | } 40 | 41 | func (m *mockMuxer) UnionResolveType(in driver.UnionResolveTypeInput) (string, error) { 42 | called := m.Called(in) 43 | return called.String(0), called.Error(1) 44 | } 45 | 46 | func (m *mockMuxer) SubscriptionConnection(in driver.SubscriptionConnectionInput) (interface{}, error) { 47 | called := m.Called(in) 48 | return called.Get(0), called.Error(1) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/cors/cors.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type CorsOptions struct { 11 | AllowedMethods, AllowedHeaders, AllowedOrigins []string 12 | AllowedCredentials bool 13 | } 14 | 15 | func retriveOriginEnv(name string) []string { 16 | return strings.Split(os.Getenv(name), " ") 17 | } 18 | 19 | func NewCors() CorsOptions { 20 | allowedOrigins := []string{"*"} 21 | if envOrigin := retriveOriginEnv("ALLOWED_ORIGINS"); envOrigin[0] != "" { 22 | allowedOrigins = envOrigin 23 | } 24 | allowedMethods := []string{http.MethodHead, 25 | http.MethodGet, 26 | http.MethodPost, 27 | http.MethodPut, 28 | http.MethodPatch, 29 | http.MethodDelete, 30 | } 31 | if envMethod := retriveOriginEnv("ALLOWED_METHODS"); envMethod[0] == "" { 32 | allowedMethods = []string{"POST", "GET", "OPTIONS"} 33 | } 34 | allowedHeaders := []string{"*"} 35 | if envHeaders := retriveOriginEnv("ALLOWED_HEADERS"); envHeaders[0] == "" { 36 | allowedHeaders = []string{"Accept", "Authorization", "Origin", "Content-Type"} 37 | } 38 | allowedCredentials := true 39 | var err error 40 | if envCredentials := os.Getenv("ALLOWED_CREDENTIALS"); envCredentials != "" { 41 | allowedCredentials, err = strconv.ParseBool(envCredentials) 42 | if err != nil { 43 | panic("cannot parse ALLOWED_CREDENTIALS env to boolean") 44 | } 45 | } 46 | c := CorsOptions{ 47 | AllowedMethods: allowedMethods, 48 | AllowedHeaders: allowedHeaders, 49 | AllowedOrigins: allowedOrigins, 50 | AllowedCredentials: allowedCredentials, 51 | } 52 | return c 53 | } 54 | -------------------------------------------------------------------------------- /pkg/parser/schema.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/graphql-go/graphql" 7 | "github.com/graphql-go/graphql/language/ast" 8 | ) 9 | 10 | type schema struct { 11 | Query *rootOperation 12 | Mutation *rootOperation 13 | Subscription *rootOperation 14 | } 15 | 16 | func analyzeSchema(schemaNode *ast.SchemaDefinition) schema { 17 | var s schema 18 | for _, op := range schemaNode.OperationTypes { 19 | switch op.Operation { 20 | case "query": 21 | s.Query = (*rootOperation)(op) 22 | case "mutation": 23 | s.Mutation = (*rootOperation)(op) 24 | case "subscription": 25 | s.Subscription = (*rootOperation)(op) 26 | } 27 | } 28 | return s 29 | } 30 | 31 | func (s schema) parse(p *Parser) (graphql.Schema, error) { 32 | if p.gqlTypeMap == nil { 33 | p.gqlTypeMap = graphql.TypeMap{} 34 | } 35 | if s.Query == nil { 36 | return graphql.Schema{}, errors.New("schema is missing root query") 37 | } 38 | o, err := s.Query.config(p) 39 | if err != nil { 40 | return graphql.Schema{}, err 41 | } 42 | sCfg := graphql.SchemaConfig{ 43 | Query: o, 44 | } 45 | if s.Mutation != nil { 46 | o, err = s.Mutation.config(p) 47 | if err != nil { 48 | return graphql.Schema{}, err 49 | } 50 | sCfg.Mutation = o 51 | } 52 | if s.Subscription != nil { 53 | o, err = s.Subscription.config(p) 54 | if err != nil { 55 | return graphql.Schema{}, err 56 | } 57 | sCfg.Subscription = o 58 | } 59 | sCfg.Types = make([]graphql.Type, 0, len(p.gqlTypeMap)) 60 | for _, t := range p.gqlTypeMap { 61 | sCfg.Types = append(sCfg.Types, t) 62 | } 63 | return graphql.NewSchema(sCfg) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/grpc/union_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/graphql-editor/stucco/pkg/grpc" 8 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func TestClientUnionResolveType(t *testing.T) { 14 | prototest.RunUnionResolveTypeClientTests(t, func(t *testing.T, tt prototest.UnionResolveTypeClientTest) { 15 | driverClientMock := new(driverClientMock) 16 | driverClientMock.On( 17 | "UnionResolveType", 18 | mock.Anything, 19 | tt.ProtoRequest, 20 | ).Return(tt.ProtoResponse, tt.ProtoError) 21 | client := grpc.Client{ 22 | Client: driverClientMock, 23 | } 24 | out := client.UnionResolveType(tt.Input) 25 | assert.Equal(t, tt.Expected, out) 26 | }) 27 | } 28 | 29 | func TestServerUnionResolveType(t *testing.T) { 30 | prototest.RunUnionResolveTypeServerTests(t, func(t *testing.T, tt prototest.UnionResolveTypeServerTest) { 31 | unionResolveTypeMock := new(unionResolveTypeMock) 32 | unionResolveTypeMock.On("Handle", tt.HandlerInput).Return(tt.HandlerOutput, tt.HandlerError) 33 | srv := grpc.Server{ 34 | UnionResolveTypeHandler: unionResolveTypeMock, 35 | } 36 | out, err := srv.UnionResolveType(context.Background(), tt.Input) 37 | assert.NoError(t, err) 38 | assert.Equal(t, tt.Expected, out) 39 | }) 40 | t.Run("RecoversFromPanic", func(t *testing.T) { 41 | srv := grpc.Server{} 42 | out, err := srv.UnionResolveType(context.Background(), nil) 43 | assert.NoError(t, err) 44 | assert.NotNil(t, out.Error) 45 | assert.NotEmpty(t, out.Error.Msg) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/authorize.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // Authorize over http 13 | func (c *Client) Authorize(input driver.AuthorizeInput) driver.AuthorizeOutput { 14 | var out driver.AuthorizeOutput 15 | var body bytes.Buffer 16 | err := protodriver.WriteAuthorizeInput(&body, input) 17 | if err == nil { 18 | var b []byte 19 | if b, err = c.do(message{ 20 | contentType: authorizeRequestMessage, 21 | responseContentType: authorizeResponseMessage, 22 | b: body.Bytes(), 23 | }); err == nil { 24 | out, err = protodriver.ReadAuthorizeOutput(bytes.NewReader(b)) 25 | } 26 | } 27 | if err != nil { 28 | out.Error = &driver.Error{ 29 | Message: err.Error(), 30 | } 31 | } 32 | return out 33 | } 34 | 35 | func (h *Handler) authorize(req *http.Request, rw http.ResponseWriter) error { 36 | rw.Header().Add(contentTypeHeader, authorizeResponseMessage.String()) 37 | in, err := protodriver.ReadAuthorizeInput(req.Body) 38 | if err == nil { 39 | req.Body.Close() 40 | if err == nil { 41 | var driverResp bool 42 | driverResp, err = h.Authorize(in) 43 | if err == nil { 44 | err = protodriver.WriteAuthorizeOutput(rw, driverResp) 45 | } 46 | } 47 | } 48 | if err != nil { 49 | err = writeProto(rw, &protoMessages.AuthorizeResponse{ 50 | Error: &protoMessages.Error{ 51 | Msg: err.Error(), 52 | }, 53 | }) 54 | } 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/grpc/subscription_listen.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/graphql-editor/stucco/pkg/driver" 5 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 6 | protoDriverService "github.com/graphql-editor/stucco_proto/go/driver_service" 7 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 8 | ) 9 | 10 | // SubscriptionListen returns a subscription event reader. 11 | func (m *Client) SubscriptionListen(input driver.SubscriptionListenInput) (out driver.SubscriptionListenOutput) { 12 | req, err := protodriver.MakeSubscriptionListenRequest(input) 13 | if err == nil { 14 | out.Reader, err = protodriver.NewSubscriptionReader(m.Client, req) 15 | } 16 | if err != nil { 17 | out.Error = &driver.Error{Message: err.Error()} 18 | } 19 | return 20 | } 21 | 22 | type subscriptionListenEmitter struct { 23 | srv protoDriverService.Driver_SubscriptionListenServer 24 | } 25 | 26 | func (s subscriptionListenEmitter) Emit() error { 27 | return s.srv.Send(&protoMessages.SubscriptionListenMessage{ 28 | Next: true, 29 | }) 30 | } 31 | 32 | func (s subscriptionListenEmitter) Close() error { 33 | return s.srv.Send(&protoMessages.SubscriptionListenMessage{ 34 | Next: false, 35 | }) 36 | } 37 | 38 | // SubscriptionListen implements protoMessages.DriverServer 39 | func (m *Server) SubscriptionListen(req *protoMessages.SubscriptionListenRequest, srv protoDriverService.Driver_SubscriptionListenServer) error { 40 | input, err := protodriver.MakeSubscriptionListenInput(req) 41 | if err == nil { 42 | err = m.SubscriptionListenHandler.Handle(input, subscriptionListenEmitter{ 43 | srv: srv, 44 | }) 45 | } 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /cmd/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 NAME HERE 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package configcmd 17 | 18 | import ( 19 | "os" 20 | "path/filepath" 21 | 22 | "github.com/graphql-editor/stucco/pkg/server" 23 | "github.com/graphql-editor/stucco/pkg/utils" 24 | "github.com/pkg/errors" 25 | "github.com/spf13/cobra" 26 | "k8s.io/klog" 27 | ) 28 | 29 | func addToConfig() (*server.Config, error) { 30 | wd, err := os.Getwd() 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | var cfg server.Config 36 | cfgPath := "" 37 | if wd != "" { 38 | cfgPath = filepath.Join(wd, "stucco") 39 | } 40 | if err := utils.LoadConfigFile(cfgPath, &cfg); err != nil { 41 | return nil, err 42 | } 43 | return &cfg, nil 44 | } 45 | 46 | // NewConfigCommand create new config command 47 | func NewConfigCommand() *cobra.Command { 48 | configCommand := &cobra.Command{ 49 | Use: "config", 50 | Short: "basic stucco config", 51 | } 52 | configCommand.AddCommand(addCommand()) 53 | return configCommand 54 | } 55 | 56 | func exitErr(err error, msg string) { 57 | klog.Fatalln(errors.Wrap(err, msg).Error()) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/driver/driver.go: -------------------------------------------------------------------------------- 1 | /*Package driver is an interface that must be implemented by concrete driver implementations 2 | of runners. 3 | */ 4 | package driver 5 | 6 | // Driver is an interface that must be defined by an implementation 7 | // for of specific runner. 8 | type Driver interface { 9 | // Authorize runs a custom auth code on function 10 | Authorize(AuthorizeInput) AuthorizeOutput 11 | // SetSecrets defined by user, it's runner's responsibility to pass them 12 | // to runtime. 13 | SetSecrets(SetSecretsInput) SetSecretsOutput 14 | // FieldResolve requests an execution of defined resolver for a field 15 | FieldResolve(FieldResolveInput) FieldResolveOutput 16 | // InterfaceResolveType requests an execution of defined interface function for a type 17 | InterfaceResolveType(InterfaceResolveTypeInput) InterfaceResolveTypeOutput 18 | // ScalarParse requests an execution of defined parse function for a scalar 19 | ScalarParse(ScalarParseInput) ScalarParseOutput 20 | // ScalarSerialize requests an execution of defined serialize function for a scalar 21 | ScalarSerialize(ScalarSerializeInput) ScalarSerializeOutput 22 | // UnionResolveType requests an execution of defined union function for a type 23 | UnionResolveType(UnionResolveTypeInput) UnionResolveTypeOutput 24 | // Stream begins streaming data between router and runner. 25 | Stream(StreamInput) StreamOutput 26 | // SubscriptionConnection creates connection payload for subscription 27 | SubscriptionConnection(SubscriptionConnectionInput) SubscriptionConnectionOutput 28 | // SubscriptionListen creates connection payload for subscription 29 | SubscriptionListen(SubscriptionListenInput) SubscriptionListenOutput 30 | } 31 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/field.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // FieldResolve over http 13 | func (c *Client) FieldResolve(input driver.FieldResolveInput) driver.FieldResolveOutput { 14 | var out driver.FieldResolveOutput 15 | var body bytes.Buffer 16 | err := protodriver.WriteFieldResolveInput(&body, input) 17 | if err == nil { 18 | var b []byte 19 | if b, err = c.do(message{ 20 | contentType: fieldResolveRequestMessage, 21 | responseContentType: fieldResolveResponseMessage, 22 | b: body.Bytes(), 23 | }); err == nil { 24 | out, err = protodriver.ReadFieldResolveOutput(bytes.NewReader(b)) 25 | } 26 | } 27 | if err != nil { 28 | out.Error = &driver.Error{ 29 | Message: err.Error(), 30 | } 31 | } 32 | return out 33 | } 34 | 35 | func (h *Handler) fieldResolve(req *http.Request, rw http.ResponseWriter) error { 36 | rw.Header().Add(contentTypeHeader, fieldResolveResponseMessage.String()) 37 | in, err := protodriver.ReadFieldResolveInput(req.Body) 38 | if err == nil { 39 | req.Body.Close() 40 | if err == nil { 41 | var driverResp interface{} 42 | driverResp, err = h.FieldResolve(in) 43 | if err == nil { 44 | err = protodriver.WriteFieldResolveOutput(rw, driverResp) 45 | } 46 | } 47 | } 48 | if err != nil { 49 | err = writeProto(rw, &protoMessages.FieldResolveResponse{ 50 | Error: &protoMessages.Error{ 51 | Msg: err.Error(), 52 | }, 53 | }) 54 | } 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/driver/stream.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | type StreamInfo struct { 6 | FieldName string `json:"fieldName"` 7 | Path *types.ResponsePath `json:"path,omitempty"` 8 | ReturnType *types.TypeRef `json:"returnType,omitempty"` 9 | ParentType *types.TypeRef `json:"parentType,omitempty"` 10 | Operation *types.OperationDefinition `json:"operation,omitempty"` 11 | VariableValues map[string]interface{} `json:"variableValues,omitempty"` 12 | } 13 | 14 | type StreamMessage struct { 15 | Response interface{} `json:"response,omitempty"` 16 | Error *Error `json:"error,"` 17 | } 18 | 19 | type StreamReader interface { 20 | // Error returns the status of stream that is no longer available for reading, if there was no error and stream was properly closed, it returns nil. 21 | Error() error 22 | // Next is blocking operation that waits until next message is available or until stream is no longer available for reading. When next message is available function returns true, otherwise it returns false. 23 | Next() bool 24 | // Read returns next message in stream. Read can only by called after Next that returned true. 25 | Read() StreamMessage 26 | // Close stream 27 | Close() 28 | } 29 | 30 | type StreamInput struct { 31 | Function types.Function 32 | Arguments types.Arguments `json:"arguments,omitempty"` 33 | Info StreamInfo `json:"info"` 34 | Secrets Secrets `json:"secrets,omitempty"` 35 | Protocol interface{} `json:"protocol,omitempty"` 36 | } 37 | 38 | type StreamOutput struct { 39 | Error *Error 40 | Reader StreamReader 41 | } 42 | -------------------------------------------------------------------------------- /pkg/driver/subscription_listen.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "github.com/graphql-editor/stucco/pkg/types" 4 | 5 | // SubscriptionListenInput represents input to a function which listen on events that trigger subscription 6 | type SubscriptionListenInput struct { 7 | Function types.Function 8 | Query string `json:"query,omitempty"` 9 | VariableValues map[string]interface{} `json:"variableValues,omitempty"` 10 | OperationName string `json:"operationName,omitempty"` 11 | Protocol interface{} `json:"protocol,omitempty"` 12 | Operation *types.OperationDefinition `json:"operation,omitempty"` 13 | } 14 | 15 | // SubscriptionListenReader is a simple interface that listens for pings from backing function 16 | type SubscriptionListenReader interface { 17 | // Error returns the status of subscription listener that is no longer available for reading, if there was no error and stream was properly closed, it returns nil. 18 | Error() error 19 | // Next is blocking call that returns true when a new subscription should be started or false when listener is finished. 20 | Next() bool 21 | // Read returns a value emited by listen reader or nil if none. Each call must be preceded by a Next call that returns true. 22 | // It is considered an error to call Next and Read asynchronously. 23 | Read() (interface{}, error) 24 | // Close closes the reader 25 | Close() error 26 | } 27 | 28 | // SubscriptionListenOutput represents response from a function which listen on events that trigger subscription 29 | type SubscriptionListenOutput struct { 30 | Error *Error `json:"error,omitempty"` 31 | Reader SubscriptionListenReader 32 | } 33 | -------------------------------------------------------------------------------- /pkg/grpc/interface_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/graphql-editor/stucco/pkg/grpc" 8 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func TestClientInterfaceResolveType(t *testing.T) { 14 | prototest.RunInterfaceResolveTypeClientTests(t, func(t *testing.T, tt prototest.InterfaceResolveTypeClientTest) { 15 | driverClientMock := new(driverClientMock) 16 | driverClientMock.On( 17 | "InterfaceResolveType", 18 | mock.Anything, 19 | tt.ProtoRequest, 20 | ).Return(tt.ProtoResponse, tt.ProtoError) 21 | client := grpc.Client{ 22 | Client: driverClientMock, 23 | } 24 | out := client.InterfaceResolveType(tt.Input) 25 | assert.Equal(t, tt.Expected, out) 26 | }) 27 | } 28 | 29 | func TestServerInterfaceResolveType(t *testing.T) { 30 | prototest.RunInterfaceResolveTypeServerTests(t, func(t *testing.T, tt prototest.InterfaceResolveTypeServerTest) { 31 | interfaceResolveTypeMock := new(interfaceResolveTypeMock) 32 | interfaceResolveTypeMock.On("Handle", tt.HandlerInput).Return(tt.HandlerOutput, tt.HandlerError) 33 | srv := grpc.Server{ 34 | InterfaceResolveTypeHandler: interfaceResolveTypeMock, 35 | } 36 | out, err := srv.InterfaceResolveType(context.Background(), tt.Input) 37 | assert.NoError(t, err) 38 | assert.Equal(t, tt.Expected, out) 39 | }) 40 | t.Run("RecoversFromPanic", func(t *testing.T) { 41 | srv := grpc.Server{} 42 | out, err := srv.InterfaceResolveType(context.Background(), nil) 43 | assert.NoError(t, err) 44 | assert.NotNil(t, out.Error) 45 | assert.NotEmpty(t, out.Error.Msg) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/grpc/field_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/graphql-editor/stucco/pkg/grpc" 8 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func TestClientFieldResolve(t *testing.T) { 14 | prototest.RunFieldResolveClientTests(t, func(t *testing.T, tt prototest.FieldResolveClientTest) { 15 | driverClientMock := new(driverClientMock) 16 | driverClientMock.On( 17 | "FieldResolve", 18 | mock.Anything, 19 | tt.ProtoRequest, 20 | ).Return(tt.ProtoResponse, nil) 21 | client := grpc.Client{ 22 | Client: driverClientMock, 23 | } 24 | out := client.FieldResolve(tt.Input) 25 | assert.Equal(t, tt.Expected, out) 26 | driverClientMock.AssertCalled(t, "FieldResolve", mock.Anything, tt.ProtoRequest) 27 | }) 28 | } 29 | 30 | func TestServerFieldResolve(t *testing.T) { 31 | prototest.RunFieldResolveServerTests(t, func(t *testing.T, tt prototest.FieldResolveServerTest) { 32 | fieldResolveMock := new(fieldResolveMock) 33 | fieldResolveMock.On("Handle", tt.HandlerInput).Return(tt.HandlerResponse, tt.HandlerError) 34 | srv := grpc.Server{ 35 | FieldResolveHandler: fieldResolveMock, 36 | } 37 | resp, err := srv.FieldResolve(context.Background(), tt.Input) 38 | assert.NoError(t, err) 39 | assert.Equal(t, tt.Expected, resp) 40 | fieldResolveMock.AssertCalled(t, "Handle", tt.HandlerInput) 41 | }) 42 | t.Run("RecoversFromPanic", func(t *testing.T) { 43 | srv := grpc.Server{} 44 | resp, err := srv.FieldResolve(context.Background(), nil) 45 | assert.NoError(t, err) 46 | assert.NotNil(t, resp.Error) 47 | assert.NotEmpty(t, resp.Error.Msg) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/utils/testdata/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolvers": { 3 | "Human.friends": { 4 | "resolve": { 5 | "name": "function.friends" 6 | } 7 | }, 8 | "Human.friendsConnection": { 9 | "resolve": { 10 | "name": "function.friendsConnection" 11 | } 12 | }, 13 | "Human.starships": { 14 | "resolve": { 15 | "name": "function.starships" 16 | } 17 | }, 18 | "Droid.friends": { 19 | "resolve": { 20 | "name": "function.friends" 21 | } 22 | }, 23 | "Droid.friendsConnection": { 24 | "resolve": { 25 | "name": "function.friendsConnection" 26 | } 27 | }, 28 | "Query.hero": { 29 | "resolve": { 30 | "name": "function.hero" 31 | } 32 | }, 33 | "Query.reviews": { 34 | "resolve": { 35 | "name": "function.reviews" 36 | } 37 | }, 38 | "Query.search": { 39 | "resolve": { 40 | "name": "function.search" 41 | } 42 | }, 43 | "Query.character": { 44 | "resolve": { 45 | "name": "function.character" 46 | } 47 | }, 48 | "Query.droid": { 49 | "resolve": { 50 | "name": "function.droid" 51 | } 52 | }, 53 | "Query.human": { 54 | "resolve": { 55 | "name": "function.human" 56 | } 57 | }, 58 | "Query.starship": { 59 | "resolve": { 60 | "name": "function.starship" 61 | } 62 | }, 63 | "Mutation.createReview": { 64 | "resolve": { 65 | "name": "function.createReview" 66 | } 67 | } 68 | }, 69 | "interfaces": { 70 | "Character": { 71 | "resolveType": { 72 | "name": "function.Character" 73 | } 74 | } 75 | }, 76 | "scalars": { 77 | "Time": { 78 | "Parse": { 79 | "name": "function.TimeParse" 80 | }, 81 | "Serialize": { 82 | "name": "function.TimeSerialize" 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/union.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // UnionResolveType over http 13 | func (c *Client) UnionResolveType(input driver.UnionResolveTypeInput) driver.UnionResolveTypeOutput { 14 | var out driver.UnionResolveTypeOutput 15 | var body bytes.Buffer 16 | err := protodriver.WriteUnionResolveTypeInput(&body, input) 17 | if err == nil { 18 | var b []byte 19 | if b, err = c.do(message{ 20 | contentType: unionResolveTypeRequestMessage, 21 | responseContentType: unionResolveTypeResponseMessage, 22 | b: body.Bytes(), 23 | }); err == nil { 24 | out, err = protodriver.ReadUnionResolveTypeOutput(bytes.NewReader(b)) 25 | } 26 | } 27 | if err != nil { 28 | out.Error = &driver.Error{ 29 | Message: err.Error(), 30 | } 31 | } 32 | return out 33 | } 34 | 35 | func (h *Handler) unionResolveType(req *http.Request, rw http.ResponseWriter) error { 36 | rw.Header().Add(contentTypeHeader, unionResolveTypeResponseMessage.String()) 37 | in, err := protodriver.ReadUnionResolveTypeInput(req.Body) 38 | if err == nil { 39 | req.Body.Close() 40 | if err == nil { 41 | var driverResp string 42 | driverResp, err = h.UnionResolveType(in) 43 | if err == nil { 44 | err = protodriver.WriteUnionResolveTypeOutput(rw, driverResp) 45 | } 46 | } 47 | } 48 | if err != nil { 49 | err = writeProto(rw, &protoMessages.UnionResolveTypeResponse{ 50 | Error: &protoMessages.Error{ 51 | Msg: err.Error(), 52 | }, 53 | }) 54 | } 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/utils/files.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | ) 11 | 12 | // IsLocal returns true if url scheme is empty or equal file 13 | func IsLocal(u *url.URL) bool { 14 | return u.Scheme == "" || u.Scheme == "file" 15 | } 16 | 17 | func fileWithSize(u *url.URL) (f *os.File, size int64, err error) { 18 | f, err = os.Open(u.Path) 19 | if err == nil { 20 | var fi os.FileInfo 21 | fi, err = os.Stat(u.Path) 22 | if err == nil { 23 | size = fi.Size() 24 | } 25 | } 26 | return 27 | } 28 | 29 | func fetchWithSize(u *url.URL) (rc io.ReadCloser, size int64, err error) { 30 | var resp *http.Response 31 | resp, err = http.Get(u.String()) 32 | if err == nil { 33 | if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 34 | err = fmt.Errorf("could not fetch %s, returned with error code %d", u.String(), resp.StatusCode) 35 | } 36 | } 37 | if err == nil { 38 | rc = resp.Body 39 | size = resp.ContentLength 40 | } 41 | return 42 | } 43 | 44 | // LocalOrRemoteReader creates a closable reader from url 45 | func LocalOrRemoteReader(u *url.URL) (rc io.ReadCloser, size int64, err error) { 46 | if IsLocal(u) { 47 | rc, size, err = fileWithSize(u) 48 | } else { 49 | rc, size, err = fetchWithSize(u) 50 | } 51 | return 52 | } 53 | 54 | // ReadLocalOrRemoteFile loads file from local storage or http depending on scheme in url 55 | func ReadLocalOrRemoteFile(fn string) (b []byte, err error) { 56 | var u *url.URL 57 | u, err = url.Parse(fn) 58 | if err == nil { 59 | var rc io.ReadCloser 60 | rc, _, err = LocalOrRemoteReader(u) 61 | if err == nil { 62 | defer rc.Close() 63 | b, err = ioutil.ReadAll(rc) 64 | } 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/interface.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // InterfaceResolveType over http 13 | func (c *Client) InterfaceResolveType(input driver.InterfaceResolveTypeInput) driver.InterfaceResolveTypeOutput { 14 | var out driver.InterfaceResolveTypeOutput 15 | var body bytes.Buffer 16 | err := protodriver.WriteInterfaceResolveTypeInput(&body, input) 17 | if err == nil { 18 | var b []byte 19 | if b, err = c.do(message{ 20 | contentType: interfaceResolveTypeRequestMessage, 21 | responseContentType: interfaceResolveTypeResponseMessage, 22 | b: body.Bytes(), 23 | }); err == nil { 24 | out, err = protodriver.ReadInterfaceResolveTypeOutput(bytes.NewReader(b)) 25 | } 26 | } 27 | if err != nil { 28 | out.Error = &driver.Error{ 29 | Message: err.Error(), 30 | } 31 | } 32 | return out 33 | } 34 | 35 | func (h *Handler) interfaceResolveType(req *http.Request, rw http.ResponseWriter) error { 36 | rw.Header().Add(contentTypeHeader, interfaceResolveTypeResponseMessage.String()) 37 | in, err := protodriver.ReadInterfaceResolveTypeInput(req.Body) 38 | if err == nil { 39 | req.Body.Close() 40 | if err == nil { 41 | var driverResp string 42 | driverResp, err = h.InterfaceResolveType(in) 43 | if err == nil { 44 | err = protodriver.WriteInterfaceResolveTypeOutput(rw, driverResp) 45 | } 46 | } 47 | } 48 | if err != nil { 49 | err = writeProto(rw, &protoMessages.InterfaceResolveTypeResponse{ 50 | Error: &protoMessages.Error{ 51 | Msg: err.Error(), 52 | }, 53 | }) 54 | } 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/parser/nonscalar.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/graphql-go/graphql" 7 | "github.com/graphql-go/graphql/language/ast" 8 | ) 9 | 10 | type namedDefintion interface { 11 | ast.Definition 12 | GetName() *ast.Name 13 | } 14 | 15 | func customDefinition(p *Parser, d ast.Definition) (gt graphql.Type, err error) { 16 | t, ok := d.(namedDefintion) 17 | if !ok { 18 | return nil, errors.New("not a type definition") 19 | } 20 | if gt, ok := p.gqlTypeMap[t.GetName().Value]; ok { 21 | return gt, nil 22 | } 23 | // Prevent recursion 24 | switch t := d.(type) { 25 | case *ast.ScalarDefinition: 26 | serialize := func(v interface{}) interface{} { 27 | return v 28 | } 29 | parseValue := func(v interface{}) interface{} { 30 | return v 31 | } 32 | if fn, ok := p.Scalars[t.Name.Value]; ok { 33 | if fn.Serialize != nil { 34 | serialize = fn.Serialize 35 | } 36 | if fn.Parse != nil { 37 | parseValue = fn.Parse 38 | } 39 | } 40 | sc := graphql.ScalarConfig{ 41 | Name: t.Name.Value, 42 | ParseValue: parseValue, 43 | ParseLiteral: func(v ast.Value) interface{} { 44 | return parseValue(v.GetValue()) 45 | }, 46 | Serialize: serialize, 47 | } 48 | setDescription(&sc.Description, t) 49 | st := graphql.NewScalar(sc) 50 | p.gqlTypeMap[st.Name()] = st 51 | gt = st 52 | case *ast.EnumDefinition: 53 | gt, err = enumDefinition(p, t) 54 | case *ast.InputObjectDefinition: 55 | gt, err = inputObjectDefinition(p, t) 56 | case *ast.InterfaceDefinition: 57 | gt, err = interfaceDefinition(p, t) 58 | case *ast.ObjectDefinition: 59 | gt, err = objectDefintion(p, t) 60 | case *ast.UnionDefinition: 61 | gt, err = unionDefinition(p, t) 62 | default: 63 | err = errors.New("unsupported type defintion") 64 | } 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/subscription_connection.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // SubscriptionConnection implements driver.SubscriptionConnection over HTTP 13 | func (c *Client) SubscriptionConnection(input driver.SubscriptionConnectionInput) driver.SubscriptionConnectionOutput { 14 | var out driver.SubscriptionConnectionOutput 15 | var body bytes.Buffer 16 | err := protodriver.WriteSubscriptionConnectionInput(&body, input) 17 | if err == nil { 18 | var b []byte 19 | if b, err = c.do(message{ 20 | contentType: subscriptionConnectionRequestMessage, 21 | responseContentType: subscriptionConnectionResponseMessage, 22 | b: body.Bytes(), 23 | }); err == nil { 24 | out, err = protodriver.ReadSubscriptionConnectionOutput(bytes.NewReader(b)) 25 | } 26 | } 27 | if err != nil { 28 | out.Error = &driver.Error{ 29 | Message: err.Error(), 30 | } 31 | } 32 | return out 33 | } 34 | 35 | func (h *Handler) subscriptionConnection(req *http.Request, rw http.ResponseWriter) error { 36 | rw.Header().Add(contentTypeHeader, subscriptionConnectionResponseMessage.String()) 37 | in, err := protodriver.ReadSubscriptionConnectionInput(req.Body) 38 | if err == nil { 39 | req.Body.Close() 40 | if err == nil { 41 | var driverResp interface{} 42 | driverResp, err = h.SubscriptionConnection(in) 43 | if err == nil { 44 | err = protodriver.WriteSubscriptionConnectionOutput(rw, driverResp) 45 | } 46 | } 47 | } 48 | if err != nil { 49 | err = writeProto(rw, &protoMessages.SubscriptionConnectionResponse{ 50 | Error: &protoMessages.Error{ 51 | Msg: err.Error(), 52 | }, 53 | }) 54 | } 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/handlers/graphiql.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/graphql-go/graphql" 7 | ) 8 | 9 | func renderGraphiQL(rw http.ResponseWriter, params graphql.Params) { 10 | _, err := rw.Write(graphiql) 11 | if err != nil { 12 | http.Error(rw, err.Error(), http.StatusInternalServerError) 13 | } 14 | return 15 | } 16 | 17 | var graphiql = []byte(` 18 | 19 | GraphiQL 20 | 21 | 22 | 23 |
24 | 25 | 29 | 33 | 37 | 38 | 62 | 63 | `) 64 | -------------------------------------------------------------------------------- /pkg/driver/plugin/plugin_posix_test.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package plugin_test 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "testing" 9 | 10 | "github.com/graphql-editor/stucco/pkg/driver/plugin" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | func TestLoadDriverPluginsCallsConfig(t *testing.T) { 15 | execMock := &execCommandContextMock{} 16 | plugin.ExecCommandContext = execMock.CommandContext 17 | oldPath := os.Getenv("PATH") 18 | // ignores bad paths, and falls back to cwd for empty path 19 | os.Setenv("PATH", string(os.PathListSeparator)+"/bad/path") 20 | 21 | // fake executable 22 | f, _ := os.Create("stucco-fake-plugin") 23 | f.Close() 24 | os.Chmod("stucco-fake-plugin", 0777) 25 | 26 | // fake bad plugin does not cause an error 27 | f, _ = os.Create("stucco-fake-bad-plugin") 28 | f.Close() 29 | os.Chmod("stucco-fake-bad-plugin", 0777) 30 | 31 | // non executables are ignored 32 | f, _ = os.Create("stucco-not-plugin") 33 | f.Close() 34 | 35 | // directories are ignored 36 | os.Mkdir("stucco-dir", 0777) 37 | defer func() { 38 | plugin.ExecCommandContext = exec.CommandContext 39 | os.Setenv("PATH", oldPath) 40 | os.Remove("stucco-fake-plugin") 41 | os.Remove("stucco-fake-bad-plugin") 42 | os.Remove("stucco-not-plugin") 43 | os.Remove("stucco-dir") 44 | }() 45 | execMock.On( 46 | "CommandContext", 47 | mock.Anything, 48 | "stucco-fake-plugin", 49 | "config", 50 | ).Return(fakeExecCommandContext) 51 | execMock.On( 52 | "CommandContext", 53 | mock.Anything, 54 | "stucco-fake-bad-plugin", 55 | "config", 56 | ).Return(fakeBadExecCommandContext) 57 | cleanup := plugin.LoadDriverPlugins(plugin.Config{}) 58 | cleanup() 59 | execMock.AssertCalled( 60 | t, 61 | "CommandContext", 62 | mock.Anything, 63 | "stucco-fake-plugin", 64 | "config", 65 | ) 66 | execMock.AssertCalled( 67 | t, 68 | "CommandContext", 69 | mock.Anything, 70 | "stucco-fake-bad-plugin", 71 | "config", 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/client.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "mime" 9 | "net/http" 10 | ) 11 | 12 | // HTTPClient for protocol buffer 13 | type HTTPClient interface { 14 | Post(url, contentType string, body io.Reader) (*http.Response, error) 15 | } 16 | 17 | // Client implements driver by using Protocol Buffers over HTTP 18 | type Client struct { 19 | HTTPClient 20 | // URL of a proto server endpoint 21 | URL string 22 | } 23 | 24 | // Config for new .Client 25 | type Config struct { 26 | Client *http.Client 27 | URL string 28 | } 29 | 30 | // NewClient creates a a new client 31 | func NewClient(config Config) Client { 32 | if config.Client == nil { 33 | config.Client = http.DefaultClient 34 | } 35 | return Client{ 36 | HTTPClient: config.Client, 37 | URL: config.URL, 38 | } 39 | } 40 | 41 | type message struct { 42 | contentType protobufMessageContentType 43 | responseContentType protobufMessageContentType 44 | b []byte 45 | } 46 | 47 | func (c *Client) do(in message) ([]byte, error) { 48 | resp, err := c.Post(c.URL, in.contentType.String(), bytes.NewReader(in.b)) 49 | if err == nil { 50 | defer resp.Body.Close() 51 | if resp.StatusCode != http.StatusOK { 52 | var b []byte 53 | b, err = ioutil.ReadAll(resp.Body) 54 | if err == nil { 55 | err = fmt.Errorf(`status_code=%d message="%s"`, resp.StatusCode, string(b)) 56 | } 57 | } 58 | } 59 | if err == nil { 60 | err = in.responseContentType.checkContentType(resp.Header.Get(contentTypeHeader)) 61 | } 62 | var b []byte 63 | if err == nil { 64 | b, err = ioutil.ReadAll(resp.Body) 65 | } 66 | return b, err 67 | } 68 | 69 | func getMessageType(contentType string) (string, error) { 70 | mediaType, params, err := mime.ParseMediaType(contentType) 71 | if err != nil { 72 | return "", err 73 | } 74 | if mediaType != protobufContentType { 75 | return "", fmt.Errorf("%s is not supported, only %s", mediaType, protobufContentType) 76 | } 77 | return params["message"], nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/grpc/set_secrets.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // SetSecrets sets a marshals secrets through GRPC 13 | func (m *Client) SetSecrets(input driver.SetSecretsInput) driver.SetSecretsOutput { 14 | var out driver.SetSecretsOutput 15 | req := protodriver.MakeSetSecretsRequest(input) 16 | var resp *protoMessages.SetSecretsResponse 17 | resp, err := m.Client.SetSecrets(context.Background(), req) 18 | if err == nil { 19 | out = protodriver.MakeSetSecretsOutput(resp) 20 | } 21 | if err != nil { 22 | out.Error = &driver.Error{Message: err.Error()} 23 | } 24 | return out 25 | } 26 | 27 | // SetSecretsHandler interface implemented by user to handle secrets input from client. 28 | type SetSecretsHandler interface { 29 | // Handle takes SetSecretsHandler as an input and should set a secrets on a server state. It should return nil if there was no error. 30 | Handle(input driver.SetSecretsInput) error 31 | } 32 | 33 | // SetSecretsHandlerFunc is a convienience wrapper around function to implement SetSecretsHandler 34 | type SetSecretsHandlerFunc func(input driver.SetSecretsInput) error 35 | 36 | // Handle takes SetSecretsInput as an input, sets a secrets state, and returns a nil if there was no error 37 | func (s SetSecretsHandlerFunc) Handle(input driver.SetSecretsInput) error { 38 | return s(input) 39 | } 40 | 41 | // SetSecrets calls user SetSecrets handler 42 | func (m *Server) SetSecrets(ctx context.Context, input *protoMessages.SetSecretsRequest) (o *protoMessages.SetSecretsResponse, _ error) { 43 | defer func() { 44 | if r := recover(); r != nil { 45 | o = &protoMessages.SetSecretsResponse{ 46 | Error: &protoMessages.Error{ 47 | Msg: fmt.Sprintf("%v", r), 48 | }, 49 | } 50 | } 51 | }() 52 | return protodriver.MakeSetSecretsResponse( 53 | m.SetSecretsHandler.Handle( 54 | protodriver.MakeSetSecretsInput(input), 55 | ), 56 | ), nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/parser/field.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/graphql-editor/stucco/pkg/utils" 7 | "github.com/graphql-go/graphql" 8 | "github.com/graphql-go/graphql/language/ast" 9 | ) 10 | 11 | func makeFieldArgs(p *Parser, fd *ast.FieldDefinition) (args graphql.FieldConfigArgument, err error) { 12 | if len(fd.Arguments) != 0 { 13 | args = make(graphql.FieldConfigArgument) 14 | for i := 0; err == nil && i < len(fd.Arguments); i++ { 15 | arg := fd.Arguments[i] 16 | var t graphql.Type 17 | if t, err = toGraphQLType(p, arg.Type); err == nil { 18 | args[arg.Name.Value] = &graphql.ArgumentConfig{ 19 | Type: t, 20 | } 21 | if arg.DefaultValue != nil { 22 | args[arg.Name.Value].DefaultValue = arg.DefaultValue.GetValue() 23 | } 24 | setDescription(&args[arg.Name.Value].Description, arg) 25 | } 26 | } 27 | } 28 | return 29 | } 30 | 31 | func makeField(p *Parser, tn string, fd *ast.FieldDefinition) (field *graphql.Field, err error) { 32 | astType, ok := fd.Type.(ast.Type) 33 | if !ok { 34 | err = errors.New("could not find field type definition") 35 | } 36 | if err == nil { 37 | var t graphql.Type 38 | t, err = toGraphQLType(p, astType) 39 | if err == nil { 40 | var args graphql.FieldConfigArgument 41 | args, err = makeFieldArgs(p, fd) 42 | if err == nil { 43 | field = &graphql.Field{ 44 | Name: fd.Name.Value, 45 | Args: args, 46 | Type: t, 47 | } 48 | if fn, ok := p.Resolvers[utils.FieldName(tn, fd.Name.Value)]; ok { 49 | field.Resolve = fn 50 | } 51 | setDescription(&field.Description, fd) 52 | } 53 | } 54 | } 55 | return 56 | } 57 | 58 | func makeInputObjectField(p *Parser, fd *ast.InputValueDefinition) (field *graphql.InputObjectFieldConfig, err error) { 59 | astType, ok := fd.Type.(ast.Type) 60 | if !ok { 61 | err = errors.New("could not find field type definition") 62 | } 63 | if err == nil { 64 | var t graphql.Type 65 | if t, err = toGraphQLType(p, astType); err == nil { 66 | field = &graphql.InputObjectFieldConfig{Type: t} 67 | setDescription(&field.Description, fd) 68 | } 69 | } 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /pkg/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/graphql-editor/stucco/pkg/driver" 5 | protoDriverService "github.com/graphql-editor/stucco_proto/go/driver_service" 6 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 7 | ) 8 | 9 | // Client for github.com/graphql-editor/stucco/pkg/proto 10 | type Client struct { 11 | Client protoDriverService.DriverClient 12 | } 13 | 14 | // StdoutHandler interface that must be implemented by user for handling 15 | // stdout bytestream requests by server. 16 | type StdoutHandler interface { 17 | Handle(*protoMessages.ByteStreamRequest, protoDriverService.Driver_StdoutServer) error 18 | } 19 | 20 | // StderrHandler interface that must be implemented by user for handling 21 | // stderr bytestream requests by server. 22 | type StderrHandler interface { 23 | Handle(*protoMessages.ByteStreamRequest, protoDriverService.Driver_StderrServer) error 24 | } 25 | 26 | // SubscriptionListenEmitter is returned to user to be called each time new subscription should be triggered. 27 | type SubscriptionListenEmitter interface { 28 | // Emit new subscription event 29 | Emit() error 30 | // Close emitter 31 | Close() error 32 | } 33 | 34 | // SubscriptionListenHandler interface that must be implemented by user for handling 35 | // subscription listen handler. 36 | type SubscriptionListenHandler interface { 37 | Handle(driver.SubscriptionListenInput, SubscriptionListenEmitter) error 38 | } 39 | 40 | // Server for github.com/graphql-editor/stucco/pkg/proto 41 | type Server struct { 42 | protoDriverService.DriverServer 43 | AuthorizeHandler AuthorizeHandler 44 | FieldResolveHandler FieldResolveHandler 45 | InterfaceResolveTypeHandler InterfaceResolveTypeHandler 46 | ScalarParseHandler ScalarParseHandler 47 | ScalarSerializeHandler ScalarSerializeHandler 48 | UnionResolveTypeHandler UnionResolveTypeHandler 49 | SetSecretsHandler SetSecretsHandler 50 | StreamHandler StreamHandler 51 | StdoutHandler StdoutHandler 52 | StderrHandler StderrHandler 53 | SubscriptionConnectionHandler SubscriptionConnectionHandler 54 | SubscriptionListenHandler SubscriptionListenHandler 55 | } 56 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/message_types.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type protobufMessageContentType string 9 | 10 | const ( 11 | protobufContentType = "application/x-protobuf" 12 | authorizeRequestMessage protobufMessageContentType = "AuthorizeRequest" 13 | authorizeResponseMessage protobufMessageContentType = "AuthorizeResponse" 14 | fieldResolveRequestMessage protobufMessageContentType = "FieldResolveRequest" 15 | fieldResolveResponseMessage protobufMessageContentType = "FieldResolveResponse" 16 | interfaceResolveTypeRequestMessage protobufMessageContentType = "InterfaceResolveTypeRequest" 17 | interfaceResolveTypeResponseMessage protobufMessageContentType = "InterfaceResolveTypeResponse" 18 | scalarParseRequestMessage protobufMessageContentType = "ScalarParseRequest" 19 | scalarParseResponseMessage protobufMessageContentType = "ScalarParseResponse" 20 | scalarSerializeRequestMessage protobufMessageContentType = "ScalarSerializeRequest" 21 | scalarSerializeResponseMessage protobufMessageContentType = "ScalarSerializeResponse" 22 | setSecretsRequestMessage protobufMessageContentType = "SetSecretsRequest" 23 | setSecretsResponseMessage protobufMessageContentType = "SetSecretsResponse" 24 | unionResolveTypeRequestMessage protobufMessageContentType = "UnionResolveTypeRequest" 25 | unionResolveTypeResponseMessage protobufMessageContentType = "UnionResolveTypeResponse" 26 | subscriptionConnectionRequestMessage protobufMessageContentType = "SubscriptionConnectionRequest" 27 | subscriptionConnectionResponseMessage protobufMessageContentType = "SubscriptionConnectionResponse" 28 | ) 29 | 30 | func (p protobufMessageContentType) String() string { 31 | return protobufContentType + "; message=" + string(p) 32 | } 33 | 34 | func (p protobufMessageContentType) checkContentType(contentType string) error { 35 | messageType, err := getMessageType(contentType) 36 | if err == nil && !strings.EqualFold(string(p), messageType) { 37 | err = fmt.Errorf("cannot unmarshal %s to %s", messageType, string(p)) 38 | } 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /pkg/providers/azure/project/router.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/graphql-editor/stucco/pkg/providers/azure/vars" 11 | "github.com/graphql-editor/stucco/pkg/utils" 12 | "github.com/graphql-editor/stucco/pkg/version" 13 | ) 14 | 15 | // Router handles tasks relating to an azure router function 16 | type Router struct { 17 | Vars *vars.Vars 18 | } 19 | 20 | // FunctionURL returns an url from which the base zip for azure router stucco function can be downloaded from 21 | func (r *Router) FunctionURL() (*url.URL, error) { 22 | v := r.Vars 23 | if v == nil { 24 | v = &vars.DefaultVars 25 | } 26 | if v.AzureFunction != "" { 27 | return url.Parse(v.AzureFunction) 28 | } 29 | ver := v.Relase.Version 30 | if ver == "" { 31 | if ver = version.Version; strings.HasPrefix(ver, "dev-") || ver == "" { 32 | ver = v.Relase.DevVersion 33 | } 34 | } 35 | return url.Parse("https://" + v.Relase.Host + "/" + ver + "/azure/function.zip") 36 | } 37 | 38 | // ZipFromReader returns a reader with router zip 39 | func (r *Router) ZipFromReader(src io.ReaderAt, size int64, extraFiles []utils.ZipData) (rc io.ReadCloser, err error) { 40 | var rd io.Reader 41 | if len(extraFiles) == 0 { 42 | var br *bytes.Reader 43 | br, err = utils.CopyToReader(utils.ReaderAtToReader(src, size)) 44 | if err == nil { 45 | rd = br 46 | } 47 | } 48 | if err == nil && rd == nil { 49 | rd, err = utils.ZipAppendFromReader(src, size, extraFiles) 50 | } 51 | if err == nil && r != nil { 52 | rc = ioutil.NopCloser(rd) 53 | } 54 | return 55 | } 56 | 57 | // Zip returns a router function as a zip reader 58 | func (r *Router) Zip(extraFiles []utils.ZipData) (rc io.ReadCloser, err error) { 59 | u, err := r.FunctionURL() 60 | if err == nil { 61 | var size int64 62 | var rrc io.ReadCloser 63 | rrc, size, err = utils.LocalOrRemoteReader(u) 64 | if err == nil { 65 | defer rrc.Close() 66 | readerAt, ok := rrc.(io.ReaderAt) 67 | if !ok { 68 | var br *bytes.Reader 69 | br, err = utils.CopyToReader(rrc) 70 | size = br.Size() 71 | readerAt = br 72 | } 73 | rc, err = r.ZipFromReader(readerAt, size, extraFiles) 74 | } 75 | } 76 | return 77 | } 78 | -------------------------------------------------------------------------------- /pkg/driver/plugin/grpc_test.go: -------------------------------------------------------------------------------- 1 | package plugin_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver/plugin" 8 | "github.com/graphql-editor/stucco/pkg/grpc" 9 | protoDriverService "github.com/graphql-editor/stucco_proto/go/driver_service" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/mock" 12 | googlegrpc "google.golang.org/grpc" 13 | ) 14 | 15 | type mockProto struct { 16 | mock.Mock 17 | } 18 | 19 | func (m *mockProto) RegisterDriverServer(s googlegrpc.ServiceRegistrar, srv protoDriverService.DriverServer) { 20 | m.Called(s, srv) 21 | } 22 | 23 | func (m *mockProto) NewDriverClient(c googlegrpc.ClientConnInterface) protoDriverService.DriverClient { 24 | return m.Called(c).Get(0).(protoDriverService.DriverClient) 25 | } 26 | 27 | func TestGRPC(t *testing.T) { 28 | t.Run("GRPCServerCallsRegisterDriverServer", func(t *testing.T) { 29 | mockProto := new(mockProto) 30 | gs := googlegrpc.NewServer() 31 | plugin.RegisterDriverServer = mockProto.RegisterDriverServer 32 | defer func() { 33 | plugin.RegisterDriverServer = protoDriverService.RegisterDriverServer 34 | }() 35 | mockProto.On( 36 | "RegisterDriverServer", 37 | gs, 38 | mock.AnythingOfType("*grpc.Server"), 39 | ) 40 | s := plugin.GRPC{} 41 | assert.NoError(t, s.GRPCServer(nil, gs)) 42 | mockProto.AssertCalled( 43 | t, 44 | "RegisterDriverServer", 45 | gs, 46 | mock.AnythingOfType("*grpc.Server"), 47 | ) 48 | }) 49 | t.Run("GRPCClientCallsNewDriverClient", func(t *testing.T) { 50 | mockProto := new(mockProto) 51 | conn := &googlegrpc.ClientConn{} 52 | driverClient := protoDriverService.NewDriverClient(conn) 53 | plugin.NewDriverClient = mockProto.NewDriverClient 54 | defer func() { 55 | plugin.NewDriverClient = protoDriverService.NewDriverClient 56 | }() 57 | mockProto.On( 58 | "NewDriverClient", 59 | conn, 60 | ).Return(driverClient) 61 | s := plugin.GRPC{} 62 | grpcClient, err := s.GRPCClient(context.Background(), nil, conn) 63 | assert.NoError(t, err) 64 | assert.IsType(t, &grpc.Client{}, grpcClient) 65 | mockProto.AssertCalled( 66 | t, 67 | "NewDriverClient", 68 | conn, 69 | ) 70 | assert.Equal(t, driverClient, grpcClient.(*grpc.Client).Client) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/driver/plugin/plugin_windows_test.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package plugin_test 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "testing" 9 | 10 | "github.com/graphql-editor/stucco/pkg/driver/plugin" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | func TestLoadDriverPluginsCallsConfig(t *testing.T) { 15 | execMock := &execCommandContextMock{} 16 | plugin.ExecCommandContext = execMock.CommandContext 17 | oldPath := os.Getenv("PATH") 18 | // ignores bad paths, and falls back to cwd for empty path 19 | os.Setenv("PATH", string(os.PathListSeparator)+"/bad/path") 20 | 21 | // fake executable 22 | f, _ := os.Create("stucco-fake-plugin.exe") 23 | f.Close() 24 | 25 | // fake script 26 | f, _ = os.Create("stucco-fake-plugin.cmd") 27 | f.Close() 28 | 29 | // fake bad plugin does not cause an error 30 | f, _ = os.Create("stucco-fake-bad-plugin.exe") 31 | f.Close() 32 | 33 | // non executables are ignored 34 | f, _ = os.Create("stucco-not-plugin") 35 | f.Close() 36 | 37 | // directories are ignored 38 | os.Mkdir("stucco-dir", 0777) 39 | defer func() { 40 | plugin.ExecCommandContext = exec.CommandContext 41 | os.Setenv("PATH", oldPath) 42 | os.Remove("stucco-fake-plugin.exe") 43 | os.Remove("stucco-fake-plugin.cmd") 44 | os.Remove("stucco-fake-bad-plugin.exe") 45 | os.Remove("stucco-not-plugin") 46 | os.Remove("stucco-dir.exe") 47 | }() 48 | execMock.On( 49 | "CommandContext", 50 | mock.Anything, 51 | "stucco-fake-plugin.exe", 52 | "config", 53 | ).Return(fakeExecCommandContext) 54 | execMock.On( 55 | "CommandContext", 56 | mock.Anything, 57 | "stucco-fake-plugin.cmd", 58 | "config", 59 | ).Return(fakeExecCommandContext) 60 | execMock.On( 61 | "CommandContext", 62 | mock.Anything, 63 | "stucco-fake-bad-plugin.exe", 64 | "config", 65 | ).Return(fakeBadExecCommandContext) 66 | cleanup := plugin.LoadDriverPlugins(plugin.Config{}) 67 | cleanup() 68 | execMock.AssertCalled( 69 | t, 70 | "CommandContext", 71 | mock.Anything, 72 | "stucco-fake-plugin.exe", 73 | "config", 74 | ) 75 | execMock.AssertCalled( 76 | t, 77 | "CommandContext", 78 | mock.Anything, 79 | "stucco-fake-plugin.cmd", 80 | "config", 81 | ) 82 | execMock.AssertCalled( 83 | t, 84 | "CommandContext", 85 | mock.Anything, 86 | "stucco-fake-bad-plugin.exe", 87 | "config", 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/grpc/authorize.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // Authorize marshals a field resolution request through GRPC to a function 13 | // that handles an actual resolution. 14 | func (m *Client) Authorize(input driver.AuthorizeInput) (f driver.AuthorizeOutput) { 15 | req, err := protodriver.MakeAuthorizeRequest(input) 16 | if err == nil { 17 | var resp *protoMessages.AuthorizeResponse 18 | resp, err = m.Client.Authorize(context.Background(), req) 19 | if err == nil { 20 | f = protodriver.MakeAuthorizeOutput(resp) 21 | } 22 | } 23 | if err != nil { 24 | f.Error = &driver.Error{Message: err.Error()} 25 | } 26 | return 27 | } 28 | 29 | // AuthorizeHandler interface implemented by user to handle field resolution request. 30 | type AuthorizeHandler interface { 31 | // Handle takes AuthorizeInput as a field resolution input and returns arbitrary 32 | // user response. 33 | Handle(input driver.AuthorizeInput) (bool, error) 34 | } 35 | 36 | // AuthorizeHandlerFunc is a convienience function wrapper implementing AuthorizeHandler 37 | type AuthorizeHandlerFunc func(input driver.AuthorizeInput) (bool, error) 38 | 39 | // Handle takes AuthorizeInput as a field resolution input and returns arbitrary 40 | func (f AuthorizeHandlerFunc) Handle(input driver.AuthorizeInput) (bool, error) { 41 | return f(input) 42 | } 43 | 44 | // Authorize function calls user implemented handler for field resolution 45 | func (m *Server) Authorize(ctx context.Context, input *protoMessages.AuthorizeRequest) (f *protoMessages.AuthorizeResponse, _ error) { 46 | defer func() { 47 | if r := recover(); r != nil { 48 | f = &protoMessages.AuthorizeResponse{ 49 | Error: &protoMessages.Error{ 50 | Msg: fmt.Sprintf("%v", r), 51 | }, 52 | } 53 | } 54 | }() 55 | req, err := protodriver.MakeAuthorizeInput(input) 56 | if err == nil { 57 | var resp bool 58 | resp, err = m.AuthorizeHandler.Handle(req) 59 | if err == nil { 60 | f = protodriver.MakeAuthorizeResponse(resp) 61 | } 62 | } 63 | if err != nil { 64 | f = &protoMessages.AuthorizeResponse{ 65 | Error: &protoMessages.Error{ 66 | Msg: err.Error(), 67 | }, 68 | } 69 | } 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /example/stucco.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolvers": { 3 | "Human.friends": { 4 | "resolve": { 5 | "name": "function.friends" 6 | } 7 | }, 8 | "Human.friendsConnection": { 9 | "resolve": { 10 | "name": "function.friendsConnection" 11 | } 12 | }, 13 | "Human.starships": { 14 | "resolve": { 15 | "name": "function.starships" 16 | } 17 | }, 18 | "Droid.friends": { 19 | "resolve": { 20 | "name": "function.friends" 21 | } 22 | }, 23 | "Droid.friendsConnection": { 24 | "resolve": { 25 | "name": "function.friendsConnection" 26 | } 27 | }, 28 | "Query.hero": { 29 | "resolve": { 30 | "name": "function.hero" 31 | } 32 | }, 33 | "Query.reviews": { 34 | "resolve": { 35 | "name": "function.reviews" 36 | } 37 | }, 38 | "Query.search": { 39 | "resolve": { 40 | "name": "function.search" 41 | } 42 | }, 43 | "Query.character": { 44 | "resolve": { 45 | "name": "function.character" 46 | } 47 | }, 48 | "Query.droid": { 49 | "resolve": { 50 | "name": "function.droid" 51 | } 52 | }, 53 | "Query.human": { 54 | "resolve": { 55 | "name": "function.human" 56 | } 57 | }, 58 | "Query.starship": { 59 | "resolve": { 60 | "name": "function.starship" 61 | } 62 | }, 63 | "Mutation.createReview": { 64 | "resolve": { 65 | "name": "function.createReview" 66 | } 67 | }, 68 | "Mutation.webhook": { 69 | "resolve": { 70 | "name": "function.webhook" 71 | } 72 | }, 73 | "Mutation.patternWebhook": { 74 | "resolve": { 75 | "name": "function.patternWebhook" 76 | }, 77 | "webhookPattern": "/{field}" 78 | }, 79 | "Subscription.randomGreet": { 80 | "resolve": { 81 | "name": "function.randomGreet" 82 | } 83 | }, 84 | "Subscription.tickAt": { 85 | "resolve": { 86 | "name": "function.tickAt" 87 | } 88 | } 89 | }, 90 | "interfaces": { 91 | "Character": { 92 | "resolveType": { 93 | "name": "function.Character" 94 | } 95 | } 96 | }, 97 | "scalars": { 98 | "Time": { 99 | "Parse": { 100 | "name": "function.TimeParse" 101 | }, 102 | "Serialize": { 103 | "name": "function.TimeSerialize" 104 | } 105 | } 106 | }, 107 | "subscriptions": { 108 | "listen": { 109 | "name": "function.listen" 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Kamil Matysiewicz 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "os" 27 | "time" 28 | 29 | crypto_rand "crypto/rand" 30 | "encoding/binary" 31 | math_rand "math/rand" 32 | 33 | "github.com/graphql-editor/stucco/cmd" 34 | azurecmd "github.com/graphql-editor/stucco/cmd/azure" 35 | localcmd "github.com/graphql-editor/stucco/cmd/local" 36 | "github.com/spf13/cobra" 37 | 38 | configcmd "github.com/graphql-editor/stucco/cmd/config" 39 | ) 40 | 41 | func seed() { 42 | // seed basic rand source 43 | // try seed it with secure seed from crypto, if it fails, fallback 44 | // to time 45 | seed := time.Now().UTC().UnixNano() 46 | var b [8]byte 47 | _, err := crypto_rand.Read(b[:]) 48 | if err == nil { 49 | seed = int64(binary.LittleEndian.Uint64(b[:])) 50 | } 51 | math_rand.Seed(seed) 52 | } 53 | 54 | func main() { 55 | seed() 56 | rootCmd := &cobra.Command{ 57 | Use: "stucco", 58 | Short: "Set of tools to work with stucco", 59 | } 60 | rootCmd.AddCommand(cmd.NewVersionCommand()) 61 | rootCmd.AddCommand(azurecmd.NewAzureCommand()) 62 | rootCmd.AddCommand(localcmd.NewLocalCommand()) 63 | rootCmd.AddCommand(configcmd.NewConfigCommand()) 64 | if err := rootCmd.Execute(); err != nil { 65 | fmt.Println(err) 66 | os.Exit(1) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/graphql-editor/stucco 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Azure/azure-storage-blob-go v0.15.0 7 | github.com/Dennor/gbtb v0.0.0-20200805082447-36c86fc7c9cb 8 | github.com/blang/semver/v4 v4.0.0 9 | github.com/bmatcuk/doublestar v1.3.1 // indirect 10 | github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251 // indirect 11 | github.com/emicklei/go-restful v2.16.0+incompatible // indirect 12 | github.com/go-logr/logr v1.2.3 // indirect 13 | github.com/google/gofuzz v1.2.0 // indirect 14 | github.com/google/uuid v1.3.0 // indirect 15 | github.com/gorilla/websocket v1.5.0 16 | github.com/graphql-editor/stucco_proto v0.7.21 17 | github.com/graphql-go/graphql v0.8.0 18 | github.com/graphql-go/handler v0.2.3 19 | github.com/hashicorp/go-hclog v1.2.1 20 | github.com/hashicorp/go-plugin v1.4.4 21 | github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect 22 | github.com/kennygrant/sanitize v1.2.4 23 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 24 | github.com/mattn/go-colorable v0.1.12 25 | github.com/mattn/go-ieproxy v0.0.7 // indirect 26 | github.com/mattn/go-isatty v0.0.14 27 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 28 | github.com/mitchellh/go-wordwrap v1.0.0 29 | github.com/oklog/run v1.1.0 // indirect 30 | github.com/pkg/errors v0.9.1 31 | github.com/prometheus/client_golang v1.12.2 // indirect 32 | github.com/prometheus/common v0.37.0 // indirect 33 | github.com/rs/cors v1.8.2 34 | github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 35 | github.com/spf13/cobra v1.5.0 36 | github.com/stretchr/objx v0.2.0 // indirect 37 | github.com/stretchr/testify v1.7.2 38 | golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect 39 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect 40 | google.golang.org/genproto v0.0.0-20220714211235-042d03aeabc9 // indirect 41 | google.golang.org/grpc v1.48.0 42 | google.golang.org/protobuf v1.28.0 43 | gopkg.in/yaml.v2 v2.4.0 44 | k8s.io/apiserver v0.24.3 45 | k8s.io/klog v1.0.0 46 | k8s.io/klog/v2 v2.70.1 // indirect 47 | k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e // indirect 48 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 49 | sigs.k8s.io/yaml v1.3.0 // indirect 50 | ) 51 | 52 | replace github.com/graphql-go/graphql => github.com/graphql-editor/graphql v0.7.10-0.20220715103515-dd2af00bb70d 53 | -------------------------------------------------------------------------------- /pkg/parser/interface.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/graphql-go/graphql" 7 | "github.com/graphql-go/graphql/language/ast" 8 | ) 9 | 10 | func getGraphQLObjectDefinition(p *Parser, odef *ast.ObjectDefinition) (o *graphql.Object, err error) { 11 | var ot graphql.Type 12 | ot, err = customDefinition(p, odef) 13 | if err == nil { 14 | var ok bool 15 | o, ok = ot.(*graphql.Object) 16 | if !ok { 17 | err = errors.New("only object can implement interface") 18 | } 19 | } 20 | return 21 | } 22 | 23 | func getObjectsImplementingInterface(p *Parser, name string) (objects []*graphql.Object, err error) { 24 | for _, definition := range p.definitions { 25 | odef, ok := definition.(*ast.ObjectDefinition) 26 | if !ok { 27 | continue 28 | } 29 | var i *ast.Named 30 | interfaces := odef.Interfaces 31 | for i == nil && len(interfaces) > 0 { 32 | if interfaces[0].Name.Value == name { 33 | i = interfaces[0] 34 | } 35 | interfaces = interfaces[1:] 36 | } 37 | if i == nil { 38 | continue 39 | } 40 | var gqlObj *graphql.Object 41 | gqlObj, err = getGraphQLObjectDefinition(p, odef) 42 | if err != nil { 43 | return 44 | } 45 | objects = append(objects, gqlObj) 46 | } 47 | return 48 | } 49 | 50 | func getInterfaceResolveTypeFunction(p *Parser, name string) (fn graphql.ResolveTypeFn, err error) { 51 | var ok bool 52 | if fn, ok = p.Interfaces[name]; !ok { 53 | var types []*graphql.Object 54 | types, err = getObjectsImplementingInterface(p, name) 55 | if err != nil { 56 | return 57 | } 58 | fn = defaultResolveFunc(types) 59 | } else { 60 | _, err = getObjectsImplementingInterface(p, name) 61 | } 62 | return 63 | } 64 | 65 | func interfaceDefinition(p *Parser, i *ast.InterfaceDefinition) (t *graphql.Interface, err error) { 66 | iCfg := graphql.InterfaceConfig{ 67 | Name: i.Name.Value, 68 | } 69 | setDescription(&iCfg.Description, i) 70 | t = graphql.NewInterface(iCfg) 71 | p.gqlTypeMap[t.Name()] = t 72 | fields := graphql.Fields{} 73 | for _, f := range i.Fields { 74 | var field *graphql.Field 75 | field, err = makeField(p, i.Name.Value, f) 76 | if err != nil { 77 | return 78 | } 79 | fields[f.Name.Value] = field 80 | } 81 | if iCfg.ResolveType, err = getInterfaceResolveTypeFunction(p, i.Name.Value); err != nil { 82 | return 83 | } 84 | iCfg.Fields = fields 85 | *t = *graphql.NewInterface(iCfg) 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /pkg/parser/union.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/graphql-go/graphql" 7 | "github.com/graphql-go/graphql/language/ast" 8 | ) 9 | 10 | func defaultResolveFunc(types []*graphql.Object) func(graphql.ResolveTypeParams) *graphql.Object { 11 | // if data has defined __typename field we can use it in default implementation 12 | // to deduce an actual type of an object 13 | return func(p graphql.ResolveTypeParams) *graphql.Object { 14 | if resolvable, ok := p.Value.(TypeResolver); ok { 15 | return resolvable.ResolveType(p, types) 16 | } 17 | if len(types) == 0 { 18 | return nil 19 | } 20 | // If only one type available, return it 21 | if len(types) == 1 { 22 | return types[0] 23 | } 24 | v := reflect.ValueOf(p.Value) 25 | for (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsValid() { 26 | v = v.Elem() 27 | } 28 | if !v.IsValid() { 29 | return nil 30 | } 31 | switch v.Kind() { 32 | case reflect.Map: 33 | rtypename := v.MapIndex(reflect.ValueOf("__typename")) 34 | if rtypename.Kind() == reflect.Interface || rtypename.Kind() == reflect.Ptr { 35 | rtypename = rtypename.Elem() 36 | } 37 | if rtypename.IsValid() && rtypename.Kind() == reflect.String { 38 | typename := rtypename.String() 39 | for _, t := range types { 40 | if t.Name() == typename { 41 | return t 42 | } 43 | } 44 | } 45 | } 46 | return nil 47 | } 48 | } 49 | 50 | type TypeResolver interface { 51 | ResolveType(p graphql.ResolveTypeParams, types []*graphql.Object) *graphql.Object 52 | } 53 | 54 | func unionDefinition(p *Parser, u *ast.UnionDefinition) (t *graphql.Union, err error) { 55 | uCfg := graphql.UnionConfig{ 56 | Name: u.Name.Value, 57 | } 58 | types := make([]*graphql.Object, 0, len(u.Types)) 59 | setDescription(&uCfg.Description, u) 60 | t = graphql.NewUnion(uCfg) 61 | p.gqlTypeMap[t.Name()] = t 62 | for _, tt := range u.Types { 63 | gt, err := toGraphQLType(p, tt) 64 | if err != nil { 65 | return nil, err 66 | } 67 | // Invariant, according to spec 68 | // only object are allowed in union 69 | ot := gt.(*graphql.Object) 70 | types = append(types, ot) 71 | } 72 | if fn, ok := p.Unions[u.Name.Value]; ok { 73 | uCfg.ResolveType = fn 74 | } else { 75 | uCfg.ResolveType = defaultResolveFunc(types) 76 | } 77 | if len(types) >= 0 { 78 | uCfg.Types = types 79 | } 80 | *t = *graphql.NewUnion(uCfg) 81 | return t, nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/handlers/webhook_handler_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/graphql-editor/stucco/pkg/handlers" 9 | "github.com/graphql-editor/stucco/pkg/router" 10 | "github.com/graphql-go/graphql" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestCreateQuery(t *testing.T) { 16 | testType1 := graphql.NewObject(graphql.ObjectConfig{ 17 | Name: "TestType1", 18 | Fields: graphql.Fields{ 19 | "field": &graphql.Field{ 20 | Type: graphql.String, 21 | }, 22 | }, 23 | }) 24 | schema, err := graphql.NewSchema(graphql.SchemaConfig{ 25 | Types: []graphql.Type{ 26 | testType1, 27 | }, 28 | Query: graphql.NewObject(graphql.ObjectConfig{ 29 | Name: "Query", 30 | Fields: graphql.Fields{ 31 | "field": &graphql.Field{ 32 | Type: graphql.String, 33 | }, 34 | "testType1Field1": &graphql.Field{ 35 | Type: testType1, 36 | }, 37 | "testType1Field2": &graphql.Field{ 38 | Type: testType1, 39 | Args: graphql.FieldConfigArgument{ 40 | "arg1": &graphql.ArgumentConfig{ 41 | Type: graphql.Int, 42 | }, 43 | "arg2": &graphql.ArgumentConfig{ 44 | Type: graphql.String, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }), 50 | }) 51 | cfg := handlers.Config{ 52 | Schema: &schema, 53 | RouterConfig: router.Config{ 54 | Resolvers: map[string]router.ResolverConfig{ 55 | "Query.testType1Field2": { 56 | Webhook: &router.WebhookConfig{ 57 | Pattern: "/{arg1}/{arg2}", 58 | }, 59 | }, 60 | }, 61 | }, 62 | } 63 | require.NoError(t, err) 64 | q, op, err := handlers.CreateQuery(cfg, &http.Request{ 65 | URL: &url.URL{ 66 | Path: "/webhook/query/field", 67 | }, 68 | }) 69 | assert.NoError(t, err) 70 | assert.Equal(t, "query{field}", q) 71 | assert.Equal(t, "query", op) 72 | 73 | q, op, err = handlers.CreateQuery(cfg, &http.Request{ 74 | URL: &url.URL{ 75 | Path: "/webhook/query/testType1Field1/field", 76 | }, 77 | }) 78 | assert.NoError(t, err) 79 | assert.Equal(t, "query{testType1Field1{field}}", q) 80 | assert.Equal(t, "query", op) 81 | 82 | q, op, err = handlers.CreateQuery(cfg, &http.Request{ 83 | URL: &url.URL{ 84 | Path: "/webhook/query/testType1Field2/1/abc/field", 85 | }, 86 | }) 87 | assert.NoError(t, err) 88 | assert.Equal(t, "query{testType1Field2(arg1: 1 arg2: \"abc\"){field}}", q) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/grpc/union.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // UnionResolveType Implements driver.Driver 13 | func (m *Client) UnionResolveType(input driver.UnionResolveTypeInput) (f driver.UnionResolveTypeOutput) { 14 | req, err := protodriver.MakeUnionResolveTypeRequest(input) 15 | if err == nil { 16 | var resp *protoMessages.UnionResolveTypeResponse 17 | resp, err = m.Client.UnionResolveType(context.Background(), req) 18 | if err == nil { 19 | f = protodriver.MakeUnionResolveTypeOutput(resp) 20 | } 21 | } 22 | if err != nil { 23 | f.Error = &driver.Error{Message: err.Error()} 24 | } 25 | return 26 | } 27 | 28 | // UnionResolveTypeHandler union implemented by user to handle union type resolution 29 | type UnionResolveTypeHandler interface { 30 | // Handle takes UnionResolveTypeInput as a type resolution input and returns 31 | // type name. 32 | Handle(driver.UnionResolveTypeInput) (string, error) 33 | } 34 | 35 | // UnionResolveTypeHandlerFunc is a convienience function wrapper implementing UnionResolveTypeHandler 36 | type UnionResolveTypeHandlerFunc func(driver.UnionResolveTypeInput) (string, error) 37 | 38 | // Handle takes UnionResolveTypeInput as a type resolution input and returns 39 | // type name. 40 | func (f UnionResolveTypeHandlerFunc) Handle(in driver.UnionResolveTypeInput) (string, error) { 41 | return f(in) 42 | } 43 | 44 | // UnionResolveType executes union type resolution request agains user defined function 45 | func (m *Server) UnionResolveType(ctx context.Context, input *protoMessages.UnionResolveTypeRequest) (f *protoMessages.UnionResolveTypeResponse, _ error) { 46 | defer func() { 47 | if r := recover(); r != nil { 48 | f = &protoMessages.UnionResolveTypeResponse{ 49 | Error: &protoMessages.Error{ 50 | Msg: fmt.Sprintf("%v", r), 51 | }, 52 | } 53 | } 54 | }() 55 | req, err := protodriver.MakeUnionResolveTypeInput(input) 56 | if err == nil { 57 | var resp string 58 | resp, err = m.UnionResolveTypeHandler.Handle(req) 59 | if err == nil { 60 | f = protodriver.MakeUnionResolveTypeResponse(resp) 61 | } 62 | } 63 | if err != nil { 64 | f = &protoMessages.UnionResolveTypeResponse{ 65 | Error: &protoMessages.Error{Msg: err.Error()}, 66 | } 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /pkg/driver/drivertest/mock.go: -------------------------------------------------------------------------------- 1 | package drivertest 2 | 3 | import ( 4 | "github.com/graphql-editor/stucco/pkg/driver" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | // MockDriver is a mock interface for testing 9 | type MockDriver struct { 10 | mock.Mock 11 | } 12 | 13 | // Authorize implements driver.Driver 14 | func (m *MockDriver) Authorize(in driver.AuthorizeInput) driver.AuthorizeOutput { 15 | return m.Called(in).Get(0).(driver.AuthorizeOutput) 16 | } 17 | 18 | // SetSecrets implements driver.Driver 19 | func (m *MockDriver) SetSecrets(in driver.SetSecretsInput) driver.SetSecretsOutput { 20 | return m.Called(in).Get(0).(driver.SetSecretsOutput) 21 | } 22 | 23 | // FieldResolve implements driver.Driver 24 | func (m *MockDriver) FieldResolve(in driver.FieldResolveInput) driver.FieldResolveOutput { 25 | return m.Called(in).Get(0).(driver.FieldResolveOutput) 26 | } 27 | 28 | // InterfaceResolveType implements driver.Driver 29 | func (m *MockDriver) InterfaceResolveType(in driver.InterfaceResolveTypeInput) driver.InterfaceResolveTypeOutput { 30 | return m.Called(in).Get(0).(driver.InterfaceResolveTypeOutput) 31 | } 32 | 33 | // ScalarParse implements driver.Driver 34 | func (m *MockDriver) ScalarParse(in driver.ScalarParseInput) driver.ScalarParseOutput { 35 | return m.Called(in).Get(0).(driver.ScalarParseOutput) 36 | } 37 | 38 | // ScalarSerialize implements driver.Driver 39 | func (m *MockDriver) ScalarSerialize(in driver.ScalarSerializeInput) driver.ScalarSerializeOutput { 40 | return m.Called(in).Get(0).(driver.ScalarSerializeOutput) 41 | } 42 | 43 | // UnionResolveType implements driver.Driver 44 | func (m *MockDriver) UnionResolveType(in driver.UnionResolveTypeInput) driver.UnionResolveTypeOutput { 45 | return m.Called(in).Get(0).(driver.UnionResolveTypeOutput) 46 | } 47 | 48 | // Stream implements driver.Driver 49 | func (m *MockDriver) Stream(in driver.StreamInput) driver.StreamOutput { 50 | return m.Called(in).Get(0).(driver.StreamOutput) 51 | } 52 | 53 | // SubscriptionListen implements driver.Driver 54 | func (m *MockDriver) SubscriptionListen(in driver.SubscriptionListenInput) driver.SubscriptionListenOutput { 55 | return m.Called(in).Get(0).(driver.SubscriptionListenOutput) 56 | } 57 | 58 | // SubscriptionConnection implements driver.Driver 59 | func (m *MockDriver) SubscriptionConnection(in driver.SubscriptionConnectionInput) driver.SubscriptionConnectionOutput { 60 | return m.Called(in).Get(0).(driver.SubscriptionConnectionOutput) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/grpc/field.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // FieldResolve marshals a field resolution request through GRPC to a function 13 | // that handles an actual resolution. 14 | func (m *Client) FieldResolve(input driver.FieldResolveInput) (f driver.FieldResolveOutput) { 15 | req, err := protodriver.MakeFieldResolveRequest(input) 16 | if err == nil { 17 | var resp *protoMessages.FieldResolveResponse 18 | resp, err = m.Client.FieldResolve(context.Background(), req) 19 | if err == nil { 20 | f = protodriver.MakeFieldResolveOutput(resp) 21 | } 22 | } 23 | if err != nil { 24 | f.Error = &driver.Error{Message: err.Error()} 25 | } 26 | return 27 | } 28 | 29 | // FieldResolveHandler interface implemented by user to handle field resolution request. 30 | type FieldResolveHandler interface { 31 | // Handle takes FieldResolveInput as a field resolution input and returns arbitrary 32 | // user response. 33 | Handle(input driver.FieldResolveInput) (interface{}, error) 34 | } 35 | 36 | // FieldResolveHandlerFunc is a convienience function wrapper implementing FieldResolveHandler 37 | type FieldResolveHandlerFunc func(input driver.FieldResolveInput) (interface{}, error) 38 | 39 | // Handle takes FieldResolveInput as a field resolution input and returns arbitrary 40 | func (f FieldResolveHandlerFunc) Handle(input driver.FieldResolveInput) (interface{}, error) { 41 | return f(input) 42 | } 43 | 44 | // FieldResolve function calls user implemented handler for field resolution 45 | func (m *Server) FieldResolve(ctx context.Context, input *protoMessages.FieldResolveRequest) (f *protoMessages.FieldResolveResponse, _ error) { 46 | defer func() { 47 | if r := recover(); r != nil { 48 | f = &protoMessages.FieldResolveResponse{ 49 | Error: &protoMessages.Error{ 50 | Msg: fmt.Sprintf("%v", r), 51 | }, 52 | } 53 | } 54 | }() 55 | req, err := protodriver.MakeFieldResolveInput(input) 56 | if err == nil { 57 | var resp interface{} 58 | resp, err = m.FieldResolveHandler.Handle(req) 59 | if err == nil { 60 | f = protodriver.MakeFieldResolveResponse(resp) 61 | } 62 | } 63 | if err != nil { 64 | f = &protoMessages.FieldResolveResponse{ 65 | Error: &protoMessages.Error{ 66 | Msg: err.Error(), 67 | }, 68 | } 69 | } 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/field_test.go: -------------------------------------------------------------------------------- 1 | package protohttp_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | protobuf "google.golang.org/protobuf/proto" 11 | "github.com/graphql-editor/stucco/pkg/driver/protohttp" 12 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 13 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 14 | "github.com/stretchr/testify/assert" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | func TestClientFieldResolve(t *testing.T) { 19 | prototest.RunFieldResolveClientTests(t, func(t *testing.T, tt prototest.FieldResolveClientTest) { 20 | srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 21 | header := req.Header.Get("content-type") 22 | assert.Equal(t, "application/x-protobuf; message=FieldResolveRequest", header) 23 | body, err := ioutil.ReadAll(req.Body) 24 | assert.NoError(t, err) 25 | req.Body.Close() 26 | var p protoMessages.FieldResolveRequest 27 | assert.NoError(t, proto.Unmarshal(body, &p)) 28 | rw.Header().Add("content-type", "application/x-protobuf; message=FieldResolveResponse") 29 | b, _ := protobuf.Marshal(tt.ProtoResponse) 30 | rw.Write(b) 31 | })) 32 | defer srv.Close() 33 | client := protohttp.NewClient(protohttp.Config{ 34 | Client: srv.Client(), 35 | URL: srv.URL, 36 | }) 37 | out := client.FieldResolve(tt.Input) 38 | assert.Equal(t, tt.Expected, out) 39 | }) 40 | } 41 | 42 | func TestServerFieldResolve(t *testing.T) { 43 | prototest.RunFieldResolveServerTests(t, func(t *testing.T, tt prototest.FieldResolveServerTest) { 44 | var r http.Request 45 | b, _ := protobuf.Marshal(tt.Input) 46 | r.Header = make(http.Header) 47 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 48 | r.Header.Add("content-type", "application/x-protobuf; message=FieldResolveRequest") 49 | responseRecorder := httptest.NewRecorder() 50 | mockMuxer := new(mockMuxer) 51 | handler := &protohttp.Handler{ 52 | Muxer: mockMuxer, 53 | } 54 | mockMuxer.On("FieldResolve", tt.HandlerInput).Return(tt.HandlerResponse, tt.HandlerError) 55 | handler.ServeHTTP(responseRecorder, &r) 56 | mockMuxer.AssertCalled(t, "FieldResolve", tt.HandlerInput) 57 | assert.Equal(t, "application/x-protobuf; message=FieldResolveResponse", responseRecorder.Header().Get("content-type")) 58 | var protoResp protoMessages.FieldResolveResponse 59 | assert.NoError(t, protobuf.Unmarshal(responseRecorder.Body.Bytes(), &protoResp)) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/driver/plugin/grpc.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/graphql-editor/stucco/pkg/grpc" 7 | protoDriverService "github.com/graphql-editor/stucco_proto/go/driver_service" 8 | "github.com/hashicorp/go-plugin" 9 | googlegrpc "google.golang.org/grpc" 10 | ) 11 | 12 | // GRPC implement GRPCPlugin interface fro go-plugin 13 | type GRPC struct { 14 | plugin.Plugin 15 | Authorize grpc.AuthorizeHandler 16 | FieldResolveHandler grpc.FieldResolveHandler 17 | InterfaceResolveTypeHandler grpc.InterfaceResolveTypeHandler 18 | ScalarParseHandler grpc.ScalarParseHandler 19 | ScalarSerializeHandler grpc.ScalarSerializeHandler 20 | UnionResolveTypeHandler grpc.UnionResolveTypeHandler 21 | SetSecretsHandler grpc.SetSecretsHandler 22 | StreamHandler grpc.StreamHandler 23 | StdoutHandler grpc.StdoutHandler 24 | StderrHandler grpc.StderrHandler 25 | SubscriptionConnectionHandler grpc.SubscriptionConnectionHandler 26 | SubscriptionListenHandler grpc.SubscriptionListenHandler 27 | } 28 | 29 | // RegisterDriverServer registers an concrete implementation of a grpc server for a protocol. 30 | var RegisterDriverServer = protoDriverService.RegisterDriverServer 31 | 32 | // GRPCServer returns a server implementation for go-plugin 33 | func (g *GRPC) GRPCServer(broker *plugin.GRPCBroker, s *googlegrpc.Server) error { 34 | RegisterDriverServer(s, &grpc.Server{ 35 | AuthorizeHandler: g.Authorize, 36 | FieldResolveHandler: g.FieldResolveHandler, 37 | InterfaceResolveTypeHandler: g.InterfaceResolveTypeHandler, 38 | ScalarParseHandler: g.ScalarParseHandler, 39 | ScalarSerializeHandler: g.ScalarSerializeHandler, 40 | UnionResolveTypeHandler: g.UnionResolveTypeHandler, 41 | StreamHandler: g.StreamHandler, 42 | StdoutHandler: g.StdoutHandler, 43 | StderrHandler: g.StderrHandler, 44 | SubscriptionConnectionHandler: g.SubscriptionConnectionHandler, 45 | SubscriptionListenHandler: g.SubscriptionListenHandler, 46 | }) 47 | return nil 48 | } 49 | 50 | // NewDriverClient creates a grpc client for protocol using connection 51 | var NewDriverClient = protoDriverService.NewDriverClient 52 | 53 | // GRPCClient returns a client implementation for go-plugin 54 | func (g *GRPC) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *googlegrpc.ClientConn) (interface{}, error) { 55 | return &grpc.Client{Client: NewDriverClient(c)}, nil 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - '**' 5 | tags-ignore: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | pull_request: 8 | branches: 9 | - master 10 | name: Stucco build 11 | env: 12 | CGO_ENABLED: 0 13 | jobs: 14 | build_cli: 15 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 16 | strategy: 17 | matrix: 18 | go-version: [1.18.x] 19 | platform: [ubuntu-latest, macos-latest, windows-latest] 20 | goarch: [amd64, 386, arm64] 21 | include: 22 | - platform: ubuntu-latest 23 | goos: linux 24 | - platform: macos-latest 25 | goos: darwin 26 | - platform: windows-latest 27 | goos: windows 28 | ext: '.exe' 29 | exclude: 30 | - platform: macos-latest 31 | goarch: 386 32 | runs-on: ${{ matrix.platform }} 33 | steps: 34 | - name: setup go 35 | uses: actions/setup-go@v1 36 | with: 37 | go-version: ${{ matrix.go-version }} 38 | - name: checkout 39 | uses: actions/checkout@v1 40 | - name: cache deps 41 | uses: actions/cache@v1 42 | with: 43 | path: ~/go/pkg/mod 44 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 45 | restore-keys: | 46 | ${{ runner.os }}-go- 47 | - name: build tasks 48 | run: go build -o tasks ./hack/tasks.go 49 | - name: build cli 50 | run: ./tasks bin/cli/${{ matrix.goos }}/${{ matrix.goarch }}/stucco${{ matrix.ext }} 51 | - name: test 52 | env: 53 | CGO_ENABLED: 1 54 | run: | 55 | ./tasks test 56 | ./tasks test-race 57 | build_azure_function: 58 | needs: 59 | - build_cli 60 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 61 | strategy: 62 | matrix: 63 | go-version: [1.18.x] 64 | platform: [ubuntu-latest] 65 | include: 66 | - platform: ubuntu-latest 67 | runs-on: ${{ matrix.platform }} 68 | steps: 69 | - name: setup go 70 | uses: actions/setup-go@v1 71 | with: 72 | go-version: ${{ matrix.go-version }} 73 | - name: checkout 74 | uses: actions/checkout@v1 75 | - name: cache deps 76 | uses: actions/cache@v1 77 | with: 78 | path: ~/go/pkg/mod 79 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 80 | restore-keys: | 81 | ${{ runner.os }}-go- 82 | - name: build tasks 83 | run: go build -o tasks ./hack/tasks.go 84 | - name: build azure function 85 | run: ./tasks build_azure_function 86 | -------------------------------------------------------------------------------- /pkg/grpc/interface.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // InterfaceResolveType handles type resolution for interface through GRPC 13 | func (m *Client) InterfaceResolveType(input driver.InterfaceResolveTypeInput) (i driver.InterfaceResolveTypeOutput) { 14 | req, err := protodriver.MakeInterfaceResolveTypeRequest(input) 15 | if err == nil { 16 | var resp *protoMessages.InterfaceResolveTypeResponse 17 | resp, err = m.Client.InterfaceResolveType(context.Background(), req) 18 | if err == nil { 19 | i = protodriver.MakeInterfaceResolveTypeOutput(resp) 20 | } 21 | } 22 | if err != nil { 23 | i.Error = &driver.Error{Message: err.Error()} 24 | err = nil 25 | } 26 | return 27 | } 28 | 29 | // InterfaceResolveTypeHandler interface implemented by user to handle interface type resolution 30 | type InterfaceResolveTypeHandler interface { 31 | // Handle takes InterfaceResolveTypeInput as a type resolution input and returns 32 | // type name. 33 | Handle(driver.InterfaceResolveTypeInput) (string, error) 34 | } 35 | 36 | // InterfaceResolveTypeHandlerFunc is a convienience function wrapper implementing InterfaceResolveTypeHandler 37 | type InterfaceResolveTypeHandlerFunc func(driver.InterfaceResolveTypeInput) (string, error) 38 | 39 | // Handle takes InterfaceResolveTypeInput as a type resolution input and returns 40 | // type name. 41 | func (f InterfaceResolveTypeHandlerFunc) Handle(in driver.InterfaceResolveTypeInput) (string, error) { 42 | return f(in) 43 | } 44 | 45 | // InterfaceResolveType handles type resolution request with user defined function 46 | func (m *Server) InterfaceResolveType(ctx context.Context, input *protoMessages.InterfaceResolveTypeRequest) (f *protoMessages.InterfaceResolveTypeResponse, _ error) { 47 | defer func() { 48 | if r := recover(); r != nil { 49 | f = &protoMessages.InterfaceResolveTypeResponse{ 50 | Error: &protoMessages.Error{ 51 | Msg: fmt.Sprintf("%v", r), 52 | }, 53 | } 54 | } 55 | }() 56 | req, err := protodriver.MakeInterfaceResolveTypeInput(input) 57 | if err == nil { 58 | var resp string 59 | resp, err = m.InterfaceResolveTypeHandler.Handle(req) 60 | if err == nil { 61 | f = protodriver.MakeInterfaceResolveTypeResponse(resp) 62 | } 63 | } 64 | if err != nil { 65 | f = &protoMessages.InterfaceResolveTypeResponse{ 66 | Error: &protoMessages.Error{Msg: err.Error()}, 67 | } 68 | } 69 | return 70 | } 71 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/secrets_test.go: -------------------------------------------------------------------------------- 1 | package protohttp_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | protobuf "google.golang.org/protobuf/proto" 11 | "github.com/graphql-editor/stucco/pkg/driver/protohttp" 12 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 13 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 14 | "github.com/stretchr/testify/assert" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | func TestClientSetSecrets(t *testing.T) { 19 | prototest.RunSetSecretsClientTests(t, func(t *testing.T, tt prototest.SetSecretsClientTest) { 20 | srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 21 | if tt.ProtoError != nil { 22 | rw.WriteHeader(http.StatusInternalServerError) 23 | rw.Write([]byte(tt.ProtoError.Error())) 24 | return 25 | } 26 | header := req.Header.Get("content-type") 27 | assert.Equal(t, "application/x-protobuf; message=SetSecretsRequest", header) 28 | body, err := ioutil.ReadAll(req.Body) 29 | assert.NoError(t, err) 30 | req.Body.Close() 31 | var p protoMessages.SetSecretsRequest 32 | assert.NoError(t, proto.Unmarshal(body, &p)) 33 | rw.Header().Add("content-type", "application/x-protobuf; message=SetSecretsResponse") 34 | b, _ := protobuf.Marshal(tt.ProtoResponse) 35 | rw.Write(b) 36 | })) 37 | defer srv.Close() 38 | client := protohttp.NewClient(protohttp.Config{ 39 | Client: srv.Client(), 40 | URL: srv.URL, 41 | }) 42 | out := client.SetSecrets(tt.Input) 43 | if tt.Expected.Error != nil { 44 | assert.Contains(t, out.Error.Message, tt.Expected.Error.Message) 45 | } else { 46 | assert.Equal(t, tt.Expected, out) 47 | } 48 | }) 49 | } 50 | 51 | func TestServerSetSecrets(t *testing.T) { 52 | prototest.RunSetSecretsServerTests(t, func(t *testing.T, tt prototest.SetSecretsServerTest) { 53 | var r http.Request 54 | b, _ := protobuf.Marshal(tt.Input) 55 | r.Header = make(http.Header) 56 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 57 | r.Header.Add("content-type", "application/x-protobuf; message=SetSecretsRequest") 58 | responseRecorder := httptest.NewRecorder() 59 | mockMuxer := new(mockMuxer) 60 | handler := &protohttp.Handler{ 61 | Muxer: mockMuxer, 62 | } 63 | mockMuxer.On("SetSecrets", tt.HandlerInput).Return(tt.HandlerOutput) 64 | handler.ServeHTTP(responseRecorder, &r) 65 | mockMuxer.AssertCalled(t, "SetSecrets", tt.HandlerInput) 66 | assert.Equal(t, "application/x-protobuf; message=SetSecretsResponse", responseRecorder.Header().Get("content-type")) 67 | var protoResp protoMessages.SetSecretsResponse 68 | assert.NoError(t, protobuf.Unmarshal(responseRecorder.Body.Bytes(), &protoResp)) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/grpc/subscription_connection.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // SubscriptionConnection marshals a field resolution request through GRPC to a function 13 | // that handles an actual resolution. 14 | func (m *Client) SubscriptionConnection(input driver.SubscriptionConnectionInput) (f driver.SubscriptionConnectionOutput) { 15 | req, err := protodriver.MakeSubscriptionConnectionRequest(input) 16 | if err == nil { 17 | var resp *protoMessages.SubscriptionConnectionResponse 18 | resp, err = m.Client.SubscriptionConnection(context.Background(), req) 19 | if err == nil { 20 | f = protodriver.MakeSubscriptionConnectionOutput(resp) 21 | } 22 | } 23 | if err != nil { 24 | f.Error = &driver.Error{Message: err.Error()} 25 | } 26 | return 27 | } 28 | 29 | // SubscriptionConnectionHandler interface implemented by user to handle subscription connection creation 30 | type SubscriptionConnectionHandler interface { 31 | // Handle takes SubscriptionConnectionInput as a field resolution input and returns 32 | // arbitrary user response. 33 | Handle(input driver.SubscriptionConnectionInput) (interface{}, error) 34 | } 35 | 36 | // SubscriptionConnectionHandlerFunc is a convienience function wrapper implementing SubscriptionConnectionHandler 37 | type SubscriptionConnectionHandlerFunc func(input driver.SubscriptionConnectionInput) (interface{}, error) 38 | 39 | // Handle takes SubscriptionConnectionInput as a field resolution input and returns arbitrary 40 | func (f SubscriptionConnectionHandlerFunc) Handle(input driver.SubscriptionConnectionInput) (interface{}, error) { 41 | return f(input) 42 | } 43 | 44 | // SubscriptionConnection function calls user implemented handler for subscription connection creation 45 | func (m *Server) SubscriptionConnection(ctx context.Context, input *protoMessages.SubscriptionConnectionRequest) (s *protoMessages.SubscriptionConnectionResponse, _ error) { 46 | defer func() { 47 | if r := recover(); r != nil { 48 | s = &protoMessages.SubscriptionConnectionResponse{ 49 | Error: &protoMessages.Error{ 50 | Msg: fmt.Sprintf("%v", r), 51 | }, 52 | } 53 | } 54 | }() 55 | req, err := protodriver.MakeSubscriptionConnectionInput(input) 56 | if err == nil { 57 | var resp interface{} 58 | resp, err = m.SubscriptionConnectionHandler.Handle(req) 59 | if err == nil { 60 | s = protodriver.MakeSubscriptionConnectionResponse(resp) 61 | } 62 | } 63 | if err != nil { 64 | s = &protoMessages.SubscriptionConnectionResponse{ 65 | Error: &protoMessages.Error{ 66 | Msg: err.Error(), 67 | }, 68 | } 69 | } 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/union_test.go: -------------------------------------------------------------------------------- 1 | package protohttp_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | protobuf "google.golang.org/protobuf/proto" 11 | "github.com/graphql-editor/stucco/pkg/driver/protohttp" 12 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 13 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 14 | "github.com/stretchr/testify/assert" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | func TestClientUnionResolveType(t *testing.T) { 19 | prototest.RunUnionResolveTypeClientTests(t, func(t *testing.T, tt prototest.UnionResolveTypeClientTest) { 20 | srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 21 | if tt.ProtoError != nil { 22 | rw.WriteHeader(http.StatusInternalServerError) 23 | rw.Write([]byte(tt.ProtoError.Error())) 24 | return 25 | } 26 | header := req.Header.Get("content-type") 27 | assert.Equal(t, "application/x-protobuf; message=UnionResolveTypeRequest", header) 28 | body, err := ioutil.ReadAll(req.Body) 29 | assert.NoError(t, err) 30 | req.Body.Close() 31 | var p protoMessages.UnionResolveTypeRequest 32 | assert.NoError(t, proto.Unmarshal(body, &p)) 33 | rw.Header().Add("content-type", "application/x-protobuf; message=UnionResolveTypeResponse") 34 | b, _ := protobuf.Marshal(tt.ProtoResponse) 35 | rw.Write(b) 36 | })) 37 | defer srv.Close() 38 | client := protohttp.NewClient(protohttp.Config{ 39 | Client: srv.Client(), 40 | URL: srv.URL, 41 | }) 42 | out := client.UnionResolveType(tt.Input) 43 | if tt.Expected.Error != nil { 44 | assert.Contains(t, out.Error.Message, tt.Expected.Error.Message) 45 | } else { 46 | assert.Equal(t, tt.Expected, out) 47 | } 48 | }) 49 | } 50 | 51 | func TestServerUnionResolveType(t *testing.T) { 52 | prototest.RunUnionResolveTypeServerTests(t, func(t *testing.T, tt prototest.UnionResolveTypeServerTest) { 53 | var r http.Request 54 | b, _ := protobuf.Marshal(tt.Input) 55 | r.Header = make(http.Header) 56 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 57 | r.Header.Add("content-type", "application/x-protobuf; message=UnionResolveTypeRequest") 58 | responseRecorder := httptest.NewRecorder() 59 | mockMuxer := new(mockMuxer) 60 | handler := &protohttp.Handler{ 61 | Muxer: mockMuxer, 62 | } 63 | mockMuxer.On("UnionResolveType", tt.HandlerInput).Return(tt.HandlerOutput, tt.HandlerError) 64 | handler.ServeHTTP(responseRecorder, &r) 65 | mockMuxer.AssertCalled(t, "UnionResolveType", tt.HandlerInput) 66 | assert.Equal(t, "application/x-protobuf; message=UnionResolveTypeResponse", responseRecorder.Header().Get("content-type")) 67 | var protoResp protoMessages.UnionResolveTypeResponse 68 | assert.NoError(t, protobuf.Unmarshal(responseRecorder.Body.Bytes(), &protoResp)) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/providers/azure/project/runtimes/stuccojs.go: -------------------------------------------------------------------------------- 1 | package runtimes 2 | 3 | import ( 4 | "bytes" 5 | "net/url" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/graphql-editor/stucco/pkg/types" 12 | "github.com/kennygrant/sanitize" 13 | ) 14 | 15 | var stuccoJSFunctionJSONTemplate = template.Must(template.New("function.json").Parse(`{ 16 | "bindings": [ 17 | { 18 | "authLevel": "anonymous", 19 | "type": "httpTrigger", 20 | "direction": "in", 21 | "name": "req", 22 | "route": "{{ .route }}", 23 | "methods": [ 24 | "get", 25 | "post" 26 | ] 27 | }, 28 | { 29 | "type": "http", 30 | "direction": "out", 31 | "name": "res" 32 | } 33 | ] 34 | }`)) 35 | 36 | // StuccoJS runtime 37 | type StuccoJS struct { 38 | OsType OsType 39 | MajorVersion int 40 | } 41 | 42 | var baseNameSeparators = regexp.MustCompile(`[./\\]`) 43 | 44 | // Function return stucco-js runtime function config 45 | func (s StuccoJS) Function(f types.Function) (files []File, err error) { 46 | parts := strings.Split(filepath.Clean(f.Name), "/") 47 | for i := range parts { 48 | parts[i] = url.PathEscape(parts[i]) 49 | } 50 | var buf bytes.Buffer 51 | if err = stuccoJSFunctionJSONTemplate.Execute(&buf, map[string]interface{}{"route": strings.Join(parts, "/")}); err == nil { 52 | files = append(files, File{ 53 | Reader: bytes.NewReader(buf.Bytes()), 54 | Path: filepath.Join( 55 | sanitize.BaseName(baseNameSeparators.ReplaceAllString(f.Name, "-")), 56 | "function.json", 57 | ), 58 | }) 59 | } 60 | return 61 | } 62 | 63 | // IgnoreFiles returns a list of Glob patterns to be ignored while creating runtime bundle 64 | func (s StuccoJS) IgnoreFiles() []string { 65 | return append([]string{"/dist/*"}, commonIgnoreList...) 66 | } 67 | 68 | // GlobalFiles returns shared config files for runtime 69 | func (s StuccoJS) GlobalFiles() ([]File, error) { 70 | return []File{ 71 | { 72 | Reader: strings.NewReader(`{ 73 | "version": "2.0", 74 | "logging": { 75 | "applicationInsights": { 76 | "samplingSettings": { 77 | "isEnabled": true, 78 | "excludedTypes": "Request" 79 | } 80 | }, 81 | "logLevel": { 82 | "default": "Information" 83 | } 84 | }, 85 | "extensionBundle": { 86 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 87 | "version": "[3.*, 4.0.0)" 88 | }, 89 | "customHandler": { 90 | "description": { 91 | "defaultExecutablePath": "node", 92 | "arguments": ["./node_modules/stucco-js/lib/cli/cli.js", "azure", "serve"] 93 | }, 94 | "enableForwardingHttpRequest": true 95 | }, 96 | "extensions": {"http": {"routePrefix": ""}} 97 | }`), 98 | Path: "host.json", 99 | }, 100 | }, nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/interface_test.go: -------------------------------------------------------------------------------- 1 | package protohttp_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | protobuf "google.golang.org/protobuf/proto" 11 | "github.com/graphql-editor/stucco/pkg/driver/protohttp" 12 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 13 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 14 | "github.com/stretchr/testify/assert" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | func TestClientInterfaceResolveType(t *testing.T) { 19 | prototest.RunInterfaceResolveTypeClientTests(t, func(t *testing.T, tt prototest.InterfaceResolveTypeClientTest) { 20 | srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 21 | if tt.ProtoError != nil { 22 | rw.WriteHeader(http.StatusInternalServerError) 23 | rw.Write([]byte(tt.ProtoError.Error())) 24 | return 25 | } 26 | header := req.Header.Get("content-type") 27 | assert.Equal(t, "application/x-protobuf; message=InterfaceResolveTypeRequest", header) 28 | body, err := ioutil.ReadAll(req.Body) 29 | assert.NoError(t, err) 30 | req.Body.Close() 31 | var p protoMessages.InterfaceResolveTypeRequest 32 | assert.NoError(t, proto.Unmarshal(body, &p)) 33 | rw.Header().Add("content-type", "application/x-protobuf; message=InterfaceResolveTypeResponse") 34 | b, _ := protobuf.Marshal(tt.ProtoResponse) 35 | rw.Write(b) 36 | })) 37 | defer srv.Close() 38 | client := protohttp.NewClient(protohttp.Config{ 39 | Client: srv.Client(), 40 | URL: srv.URL, 41 | }) 42 | out := client.InterfaceResolveType(tt.Input) 43 | if tt.Expected.Error != nil { 44 | assert.Contains(t, out.Error.Message, tt.Expected.Error.Message) 45 | } else { 46 | assert.Equal(t, tt.Expected, out) 47 | } 48 | }) 49 | } 50 | 51 | func TestServerInterfaceResolveType(t *testing.T) { 52 | prototest.RunInterfaceResolveTypeServerTests(t, func(t *testing.T, tt prototest.InterfaceResolveTypeServerTest) { 53 | var r http.Request 54 | b, _ := protobuf.Marshal(tt.Input) 55 | r.Header = make(http.Header) 56 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 57 | r.Header.Add("content-type", "application/x-protobuf; message=InterfaceResolveTypeRequest") 58 | responseRecorder := httptest.NewRecorder() 59 | mockMuxer := new(mockMuxer) 60 | handler := &protohttp.Handler{ 61 | Muxer: mockMuxer, 62 | } 63 | mockMuxer.On("InterfaceResolveType", tt.HandlerInput).Return(tt.HandlerOutput, tt.HandlerError) 64 | handler.ServeHTTP(responseRecorder, &r) 65 | mockMuxer.AssertCalled(t, "InterfaceResolveType", tt.HandlerInput) 66 | assert.Equal(t, "application/x-protobuf; message=InterfaceResolveTypeResponse", responseRecorder.Header().Get("content-type")) 67 | var protoResp protoMessages.InterfaceResolveTypeResponse 68 | assert.NoError(t, protobuf.Unmarshal(responseRecorder.Body.Bytes(), &protoResp)) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/grpc/scalar_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/graphql-editor/stucco/pkg/grpc" 8 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func TestClientScalarParse(t *testing.T) { 14 | prototest.RunScalarParseClientTests(t, func(t *testing.T, tt prototest.ScalarParseClientTest) { 15 | driverClientMock := new(driverClientMock) 16 | driverClientMock.On( 17 | "ScalarParse", 18 | mock.Anything, 19 | tt.ProtoRequest, 20 | ).Return(tt.ProtoResponse, tt.ProtoError) 21 | client := grpc.Client{ 22 | Client: driverClientMock, 23 | } 24 | out := client.ScalarParse(tt.Input) 25 | assert.Equal(t, tt.Expected, out) 26 | }) 27 | } 28 | 29 | func TestClientScalarSerialize(t *testing.T) { 30 | prototest.RunScalarSerializeClientTests(t, func(t *testing.T, tt prototest.ScalarSerializeClientTest) { 31 | driverClientMock := new(driverClientMock) 32 | driverClientMock.On( 33 | "ScalarSerialize", 34 | mock.Anything, 35 | tt.ProtoRequest, 36 | ).Return(tt.ProtoResponse, tt.ProtoError) 37 | client := grpc.Client{ 38 | Client: driverClientMock, 39 | } 40 | out := client.ScalarSerialize(tt.Input) 41 | assert.Equal(t, tt.Expected, out) 42 | }) 43 | } 44 | 45 | func TestServerScalarParse(t *testing.T) { 46 | prototest.RunScalarParseServerTests(t, func(t *testing.T, tt prototest.ScalarParseServerTest) { 47 | scalarParseMock := new(scalarParseMock) 48 | scalarParseMock.On( 49 | "Handle", 50 | tt.HandlerInput, 51 | ).Return(tt.HandlerOutput, tt.HandlerError) 52 | srv := grpc.Server{ 53 | ScalarParseHandler: scalarParseMock, 54 | } 55 | out, err := srv.ScalarParse(context.Background(), tt.Input) 56 | assert.NoError(t, err) 57 | assert.Equal(t, tt.Expected, out) 58 | }) 59 | t.Run("RecoversFromPanic", func(t *testing.T) { 60 | srv := grpc.Server{} 61 | out, err := srv.ScalarParse(context.Background(), nil) 62 | assert.NoError(t, err) 63 | assert.NotNil(t, out.Error) 64 | assert.NotEmpty(t, out.Error.Msg) 65 | }) 66 | } 67 | 68 | func TestServerScalarSerialize(t *testing.T) { 69 | prototest.RunScalarSerializeServerTests(t, func(t *testing.T, tt prototest.ScalarSerializeServerTest) { 70 | scalarSerializeMock := new(scalarSerializeMock) 71 | scalarSerializeMock.On( 72 | "Handle", 73 | tt.HandlerInput, 74 | ).Return(tt.HandlerOutput, tt.HandlerError) 75 | srv := grpc.Server{ 76 | ScalarSerializeHandler: scalarSerializeMock, 77 | } 78 | out, err := srv.ScalarSerialize(context.Background(), tt.Input) 79 | assert.NoError(t, err) 80 | assert.Equal(t, tt.Expected, out) 81 | }) 82 | t.Run("RecoversFromPanic", func(t *testing.T) { 83 | srv := grpc.Server{} 84 | out, err := srv.ScalarSerialize(context.Background(), nil) 85 | assert.NoError(t, err) 86 | assert.NotNil(t, out.Error) 87 | assert.NotEmpty(t, out.Error.Msg) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/driver/plugin/plugin_log.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/hashicorp/go-hclog" 9 | "k8s.io/klog" 10 | ) 11 | 12 | type klogLogger struct { 13 | hclog.Logger 14 | bytes.Buffer 15 | writeLock sync.Mutex 16 | trimTimeFormat int 17 | namedLock sync.Mutex 18 | namedLoggers map[string]hclog.Logger 19 | name string 20 | } 21 | 22 | func (k *klogLogger) clean(lv hclog.Level) string { 23 | parts := strings.Split(k.String(), " ")[2:] 24 | for parts[0] == "" { 25 | parts = parts[1:] 26 | } 27 | return strings.Join(parts, " ") 28 | } 29 | 30 | // Trace writes an error log to klog 31 | func (k *klogLogger) Trace(msg string, args ...interface{}) { 32 | k.writeLock.Lock() 33 | defer k.writeLock.Unlock() 34 | k.Logger.Trace(msg, args...) 35 | defer k.Reset() 36 | klog.Error(k.clean(hclog.Trace)) 37 | } 38 | 39 | // Debug writes info log to klog with verbosity of level 5 40 | func (k *klogLogger) Debug(msg string, args ...interface{}) { 41 | if klog.V(5) { 42 | k.writeLock.Lock() 43 | defer k.writeLock.Unlock() 44 | k.Logger.Debug(msg, args...) 45 | defer k.Reset() 46 | klog.V(5).Info(k.clean(hclog.Debug)) 47 | } 48 | } 49 | 50 | // Info writes info log to klog with verbosity of level 3 51 | func (k *klogLogger) Info(msg string, args ...interface{}) { 52 | if klog.V(3) { 53 | k.writeLock.Lock() 54 | defer k.writeLock.Unlock() 55 | k.Logger.Info(msg, args...) 56 | defer k.Reset() 57 | klog.V(3).Info(k.clean(hclog.Info)) 58 | } 59 | } 60 | 61 | // Warn writes a warn log to klog 62 | func (k *klogLogger) Warn(msg string, args ...interface{}) { 63 | k.writeLock.Lock() 64 | defer k.writeLock.Unlock() 65 | k.Logger.Warn(msg, args...) 66 | defer k.Reset() 67 | klog.Warning(k.clean(hclog.Info)) 68 | } 69 | 70 | // Error writes an error log to klog 71 | func (k *klogLogger) Error(msg string, args ...interface{}) { 72 | k.writeLock.Lock() 73 | defer k.writeLock.Unlock() 74 | k.Logger.Error(msg, args...) 75 | defer k.Reset() 76 | klog.Error(k.clean(hclog.Error)) 77 | } 78 | 79 | // Named returns a name of a logger for plugin 80 | func (k *klogLogger) Named(name string) hclog.Logger { 81 | k.namedLock.Lock() 82 | defer k.namedLock.Unlock() 83 | if _, ok := k.namedLoggers[name]; !ok { 84 | k.namedLoggers[name] = NewLogger(k.name + "." + name) 85 | } 86 | return k.namedLoggers[name] 87 | } 88 | 89 | // NewLogger replaces hclog used by go-plugin with klog for consistency 90 | func NewLogger(name string) hclog.Logger { 91 | l := &klogLogger{ 92 | trimTimeFormat: len(hclog.TimeFormat), 93 | name: name, 94 | namedLoggers: make(map[string]hclog.Logger), 95 | } 96 | l.Logger = hclog.New(&hclog.LoggerOptions{ 97 | Output: l, 98 | // doesn't matter, we check verbosity levels on klog 99 | // before writting either way, so always Debug is fine 100 | Level: hclog.Trace, 101 | Name: l.name, 102 | }) 103 | return l 104 | } 105 | -------------------------------------------------------------------------------- /pkg/router/router_context_extension.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/graphql-go/graphql" 7 | "github.com/graphql-go/graphql/gqlerrors" 8 | ) 9 | 10 | type routerContext int 11 | 12 | // ContextKey returns shared router context from context associated with request 13 | const ContextKey routerContext = 0 14 | 15 | // Context context associated with request 16 | type Context struct { 17 | Error error 18 | } 19 | 20 | type baseExtension struct{} 21 | 22 | func (r baseExtension) Init(ctx context.Context, p *graphql.Params) context.Context { 23 | return ctx 24 | } 25 | func (r baseExtension) ParseDidStart(ctx context.Context) (context.Context, graphql.ParseFinishFunc) { 26 | return ctx, func(err error) {} 27 | } 28 | 29 | func (r baseExtension) ValidationDidStart(ctx context.Context) (context.Context, graphql.ValidationFinishFunc) { 30 | return ctx, func([]gqlerrors.FormattedError) {} 31 | } 32 | 33 | func (r baseExtension) ExecutionDidStart(ctx context.Context) (context.Context, graphql.ExecutionFinishFunc) { 34 | return ctx, func(r *graphql.Result) {} 35 | } 36 | 37 | func (r baseExtension) ResolveFieldDidStart(ctx context.Context, info *graphql.ResolveInfo) (context.Context, graphql.ResolveFieldFinishFunc) { 38 | return ctx, func(interface{}, error) {} 39 | } 40 | 41 | func (r baseExtension) HasResult(ctx context.Context) bool { 42 | return false 43 | } 44 | 45 | func (r baseExtension) GetResult(ctx context.Context) interface{} { 46 | return nil 47 | } 48 | 49 | type routerStartContext struct { 50 | baseExtension 51 | } 52 | 53 | func (r routerStartContext) Init(ctx context.Context, p *graphql.Params) context.Context { 54 | ctx = context.WithValue(ctx, ContextKey, &Context{}) 55 | return ctx 56 | } 57 | func (r routerStartContext) Name() string { return "RouterStartExtension" } 58 | 59 | type routerFinishContext struct { 60 | baseExtension 61 | } 62 | 63 | func (r routerFinishContext) Name() string { return "RouterFinishExtension" } 64 | 65 | func (r routerFinishContext) ExecutionDidStart(ctx context.Context) (context.Context, graphql.ExecutionFinishFunc) { 66 | assertRouterOk(ctx) 67 | return ctx, func(r *graphql.Result) { 68 | if err := getRouterError(ctx); err != nil { 69 | r.Data = nil 70 | r.Errors = nil 71 | panic(err) 72 | } 73 | } 74 | } 75 | 76 | func getRouterContext(ctx context.Context) *Context { 77 | if ctx == nil { 78 | return nil 79 | } 80 | return ctx.Value(ContextKey).(*Context) 81 | } 82 | 83 | func getRouterError(ctx context.Context) error { 84 | var err error 85 | if rtContext := getRouterContext(ctx); rtContext != nil && rtContext.Error != nil { 86 | err = rtContext.Error 87 | } 88 | return err 89 | } 90 | 91 | func assertRouterOk(ctx context.Context) { 92 | if err := getRouterError(ctx); err != nil { 93 | panic(err) 94 | } 95 | } 96 | 97 | func routerError(ctx context.Context, err error) { 98 | if rtContext := getRouterContext(ctx); rtContext.Error == nil { 99 | rtContext.Error = err 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/scalar.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // ScalarParse over http 13 | func (c *Client) ScalarParse(input driver.ScalarParseInput) driver.ScalarParseOutput { 14 | var out driver.ScalarParseOutput 15 | var body bytes.Buffer 16 | err := protodriver.WriteScalarParseInput(&body, input) 17 | if err == nil { 18 | var b []byte 19 | if b, err = c.do(message{ 20 | contentType: scalarParseRequestMessage, 21 | responseContentType: scalarParseResponseMessage, 22 | b: body.Bytes(), 23 | }); err == nil { 24 | out, err = protodriver.ReadScalarParseOutput(bytes.NewReader(b)) 25 | } 26 | } 27 | if err != nil { 28 | out.Error = &driver.Error{ 29 | Message: err.Error(), 30 | } 31 | } 32 | return out 33 | } 34 | 35 | // ScalarSerialize over http 36 | func (c *Client) ScalarSerialize(input driver.ScalarSerializeInput) driver.ScalarSerializeOutput { 37 | var out driver.ScalarSerializeOutput 38 | var body bytes.Buffer 39 | err := protodriver.WriteScalarSerializeInput(&body, input) 40 | if err == nil { 41 | var b []byte 42 | if b, err = c.do(message{ 43 | contentType: scalarSerializeRequestMessage, 44 | responseContentType: scalarSerializeResponseMessage, 45 | b: body.Bytes(), 46 | }); err == nil { 47 | out, err = protodriver.ReadScalarSerializeOutput(bytes.NewReader(b)) 48 | } 49 | } 50 | if err != nil { 51 | out.Error = &driver.Error{ 52 | Message: err.Error(), 53 | } 54 | } 55 | return out 56 | } 57 | 58 | func (h *Handler) scalarParse(req *http.Request, rw http.ResponseWriter) error { 59 | rw.Header().Add(contentTypeHeader, scalarParseResponseMessage.String()) 60 | in, err := protodriver.ReadScalarParseInput(req.Body) 61 | if err == nil { 62 | req.Body.Close() 63 | if err == nil { 64 | var driverResp interface{} 65 | driverResp, err = h.ScalarParse(in) 66 | if err == nil { 67 | err = protodriver.WriteScalarParseOutput(rw, driverResp) 68 | } 69 | } 70 | } 71 | if err != nil { 72 | err = writeProto(rw, &protoMessages.ScalarParseResponse{ 73 | Error: &protoMessages.Error{ 74 | Msg: err.Error(), 75 | }, 76 | }) 77 | } 78 | return err 79 | } 80 | 81 | func (h *Handler) scalarSerialize(req *http.Request, rw http.ResponseWriter) error { 82 | rw.Header().Add(contentTypeHeader, scalarSerializeResponseMessage.String()) 83 | in, err := protodriver.ReadScalarSerializeInput(req.Body) 84 | if err == nil { 85 | req.Body.Close() 86 | if err == nil { 87 | var driverResp interface{} 88 | driverResp, err = h.ScalarSerialize(in) 89 | if err == nil { 90 | err = protodriver.WriteScalarSerializeOutput(rw, driverResp) 91 | } 92 | } 93 | } 94 | if err != nil { 95 | err = writeProto(rw, &protoMessages.ScalarSerializeResponse{ 96 | Error: &protoMessages.Error{ 97 | Msg: err.Error(), 98 | }, 99 | }) 100 | } 101 | return err 102 | } 103 | -------------------------------------------------------------------------------- /pkg/proto/prototest/set_secrets.go: -------------------------------------------------------------------------------- 1 | package prototest 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 9 | ) 10 | 11 | // SetSecretsClientTest is basic struct for testing clients implementing proto 12 | 13 | type SetSecretsClientTest struct { 14 | Title string 15 | Input driver.SetSecretsInput 16 | ProtoRequest *protoMessages.SetSecretsRequest 17 | ProtoResponse *protoMessages.SetSecretsResponse 18 | ProtoError error 19 | Expected driver.SetSecretsOutput 20 | } 21 | 22 | // SetSecretsClientTestData is a data for testing secrets of proto clients 23 | func SetSecretsClientTestData() []SetSecretsClientTest { 24 | return []SetSecretsClientTest{ 25 | { 26 | Title: "Sets secrets", 27 | Input: driver.SetSecretsInput{ 28 | Secrets: driver.Secrets{ 29 | "secret": "value", 30 | }, 31 | }, 32 | ProtoRequest: &protoMessages.SetSecretsRequest{ 33 | Secrets: []*protoMessages.Secret{ 34 | &protoMessages.Secret{ 35 | Key: "secret", 36 | Value: "value", 37 | }, 38 | }, 39 | }, 40 | ProtoResponse: new(protoMessages.SetSecretsResponse), 41 | }, 42 | { 43 | Title: "ReturnsProtoError", 44 | Input: driver.SetSecretsInput{}, 45 | ProtoRequest: new(protoMessages.SetSecretsRequest), 46 | ProtoError: errors.New("proto error"), 47 | ProtoResponse: new(protoMessages.SetSecretsResponse), 48 | Expected: driver.SetSecretsOutput{ 49 | Error: &driver.Error{ 50 | Message: "proto error", 51 | }, 52 | }, 53 | }, 54 | } 55 | } 56 | 57 | // RunSetSecretsClientTests runs all client tests on a function 58 | func RunSetSecretsClientTests(t *testing.T, f func(t *testing.T, tt SetSecretsClientTest)) { 59 | for _, tt := range SetSecretsClientTestData() { 60 | t.Run(tt.Title, func(t *testing.T) { 61 | f(t, tt) 62 | }) 63 | } 64 | } 65 | 66 | // SetSecretsServerTest is basic struct for testing clients implementing proto 67 | type SetSecretsServerTest struct { 68 | Title string 69 | Input *protoMessages.SetSecretsRequest 70 | HandlerInput driver.SetSecretsInput 71 | HandlerOutput error 72 | Expected *protoMessages.SetSecretsResponse 73 | } 74 | 75 | // SetSecretsServerTestData is a data for testing secrets of proto clients 76 | func SetSecretsServerTestData() []SetSecretsServerTest { 77 | return []SetSecretsServerTest{ 78 | { 79 | Title: "CallsHandler", 80 | Input: &protoMessages.SetSecretsRequest{ 81 | Secrets: []*protoMessages.Secret{ 82 | &protoMessages.Secret{ 83 | Key: "secret", 84 | Value: "value", 85 | }, 86 | }, 87 | }, 88 | HandlerInput: driver.SetSecretsInput{ 89 | Secrets: driver.Secrets{ 90 | "secret": "value", 91 | }, 92 | }, 93 | Expected: new(protoMessages.SetSecretsResponse), 94 | }, 95 | } 96 | } 97 | 98 | // RunSetSecretsServerTests runs all client tests on a function 99 | func RunSetSecretsServerTests(t *testing.T, f func(t *testing.T, tt SetSecretsServerTest)) { 100 | for _, tt := range SetSecretsServerTestData() { 101 | t.Run(tt.Title, func(t *testing.T) { 102 | f(t, tt) 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/router/router_test.go: -------------------------------------------------------------------------------- 1 | package router_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/graphql-editor/stucco/pkg/driver" 7 | "github.com/graphql-editor/stucco/pkg/driver/drivertest" 8 | "github.com/graphql-editor/stucco/pkg/router" 9 | "github.com/graphql-editor/stucco/pkg/types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | func TestNewRouter(t *testing.T) { 15 | defaultEnvironment := router.DefaultEnvironment() 16 | data := []struct { 17 | title string 18 | in router.Config 19 | expected router.Router 20 | expectedErr assert.ErrorAssertionFunc 21 | }{ 22 | { 23 | title: "Defaults", 24 | in: router.Config{ 25 | Interfaces: map[string]router.InterfaceConfig{ 26 | "SomeInterface": {ResolveType: types.Function{Name: "function"}}, 27 | }, 28 | Resolvers: map[string]router.ResolverConfig{ 29 | "SomeType.field": {Resolve: types.Function{Name: "function"}}, 30 | }, 31 | Scalars: map[string]router.ScalarConfig{ 32 | "SomeScalar": { 33 | Parse: types.Function{Name: "function"}, 34 | Serialize: types.Function{Name: "function"}, 35 | }, 36 | }, 37 | Unions: map[string]router.UnionConfig{ 38 | "SomeUnion": {ResolveType: types.Function{Name: "function"}}, 39 | }, 40 | Schema: ` 41 | interface SomeInterface{ 42 | field: String 43 | } 44 | type SomeType { 45 | field: String 46 | } 47 | scalar SomeScalar 48 | union SomeUnion = SomeType 49 | schema { 50 | query: SomeType 51 | } 52 | `, 53 | }, 54 | expected: router.Router{ 55 | Interfaces: map[string]router.InterfaceConfig{ 56 | "SomeInterface": { 57 | Environment: &defaultEnvironment, 58 | ResolveType: types.Function{Name: "function"}, 59 | }, 60 | }, 61 | Resolvers: map[string]router.ResolverConfig{ 62 | "SomeType.field": { 63 | Environment: &defaultEnvironment, 64 | Resolve: types.Function{Name: "function"}, 65 | }, 66 | }, 67 | Scalars: map[string]router.ScalarConfig{ 68 | "SomeScalar": { 69 | Environment: &defaultEnvironment, 70 | Parse: types.Function{Name: "function"}, 71 | Serialize: types.Function{Name: "function"}, 72 | }, 73 | }, 74 | Unions: map[string]router.UnionConfig{ 75 | "SomeUnion": { 76 | Environment: &defaultEnvironment, 77 | ResolveType: types.Function{Name: "function"}, 78 | }, 79 | }, 80 | }, 81 | expectedErr: assert.NoError, 82 | }, 83 | } 84 | for i := range data { 85 | tt := data[i] 86 | t.Run(tt.title, func(t *testing.T) { 87 | mockDefaultDriver := driver.Config{ 88 | Provider: defaultEnvironment.Provider, 89 | Runtime: defaultEnvironment.Runtime, 90 | } 91 | mockDriver := new(drivertest.MockDriver) 92 | mockDriver.On("SetSecrets", mock.Anything).Return(driver.SetSecretsOutput{}, nil) 93 | driver.Register(mockDefaultDriver, mockDriver) 94 | out, err := router.NewRouter(tt.in) 95 | tt.expectedErr(t, err) 96 | assert.Equal(t, tt.expected.Interfaces, out.Interfaces) 97 | assert.Equal(t, tt.expected.Resolvers, out.Resolvers) 98 | assert.Equal(t, tt.expected.Scalars, out.Scalars) 99 | assert.Equal(t, tt.expected.Unions, out.Unions) 100 | assert.NotNil(t, out.Schema.QueryType()) 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkg/driver/plugin/plugin_log_test.go: -------------------------------------------------------------------------------- 1 | package plugin_test 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/graphql-editor/stucco/pkg/driver/plugin" 12 | "github.com/stretchr/testify/assert" 13 | "k8s.io/klog" 14 | ) 15 | 16 | func TestHCLogToKLogRedirect(t *testing.T) { 17 | flagSet := flag.NewFlagSet("klogflags", flag.ContinueOnError) 18 | klog.InitFlags(flagSet) 19 | flagSet.Parse([]string{"-v=5"}) 20 | oldStderr := os.Stderr 21 | defer func() { 22 | os.Stderr = oldStderr 23 | }() 24 | pr, pw, _ := os.Pipe() 25 | os.Stderr = pw 26 | data := []struct { 27 | level string 28 | inMsg string 29 | inArgs []interface{} 30 | expected string 31 | }{ 32 | { 33 | level: "Trace", 34 | inMsg: "trace message\ntrace message line 2", 35 | inArgs: []interface{}{"arg1", "value1"}, 36 | expected: "^E.*] logger.name: trace message$\ntrace message line 2: arg1=value1", 37 | }, 38 | { 39 | level: "Info", 40 | inMsg: "info message\ninfo message line 2", 41 | inArgs: []interface{}{"arg1", "value1"}, 42 | expected: "^I.*] logger.name: info message$\ninfo message line 2: arg1=value1", 43 | }, 44 | { 45 | level: "Debug", 46 | inMsg: "debug message\ndebug message line 2", 47 | inArgs: []interface{}{"arg1", "value1"}, 48 | expected: "^I.*] logger.name: debug message$\ndebug message line 2: arg1=value1", 49 | }, 50 | { 51 | level: "Warn", 52 | inMsg: "warn message\nwarn message line 2", 53 | inArgs: []interface{}{"arg1", "value1"}, 54 | expected: "^W.*] logger.name: warn message$\nwarn message line 2: arg1=value1", 55 | }, 56 | { 57 | level: "Error", 58 | inMsg: "error message\nerror message line 2", 59 | inArgs: []interface{}{"arg1", "value1"}, 60 | expected: "^E.*] logger.name: error message$\nerror message line 2: arg1=value1", 61 | }, 62 | } 63 | logger := plugin.NewLogger("logger").Named("name") 64 | t.Run("Levels", func(t *testing.T) { 65 | for i := range data { 66 | tt := data[i] 67 | t.Run(tt.level, func(t *testing.T) { 68 | t.Parallel() 69 | var f func(string, ...interface{}) 70 | switch tt.level { 71 | case "Trace": 72 | f = logger.Trace 73 | case "Info": 74 | f = logger.Info 75 | case "Debug": 76 | f = logger.Debug 77 | case "Warn": 78 | f = logger.Warn 79 | case "Error": 80 | f = logger.Error 81 | } 82 | f(tt.inMsg, tt.inArgs...) 83 | }) 84 | } 85 | }) 86 | klog.Flush() 87 | pw.Close() 88 | b, _ := ioutil.ReadAll(pr) 89 | lines := strings.Split(string(b), "\n") 90 | lines = lines[:len(lines)-1] 91 | for _, tt := range data { 92 | assert.Condition(t, func() bool { 93 | expectedLines := strings.Split(tt.expected, "\n") 94 | re := regexp.MustCompile(expectedLines[0]) 95 | for i := 0; i < len(lines); i++ { 96 | if re.Match([]byte(lines[i])) { 97 | forward := 1 98 | for j := forward; j < len(expectedLines); j++ { 99 | if lines[i+j] == expectedLines[j] { 100 | forward = j + 1 101 | } 102 | } 103 | if forward == len(expectedLines) { 104 | lines = append(lines[:i], lines[i+forward:]...) 105 | return true 106 | } 107 | } 108 | } 109 | return false 110 | }) 111 | } 112 | assert.Len(t, lines, 0) 113 | } 114 | -------------------------------------------------------------------------------- /pkg/proto/driver/secrets.go: -------------------------------------------------------------------------------- 1 | package protodriver 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | 7 | protobuf "google.golang.org/protobuf/proto" 8 | 9 | "github.com/graphql-editor/stucco/pkg/driver" 10 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 11 | ) 12 | 13 | // MakeSetSecretsRequest creates protoMessages.SetSecretsRequest from driver.SetSecretsInput 14 | func MakeSetSecretsRequest(input driver.SetSecretsInput) *protoMessages.SetSecretsRequest { 15 | s := new(protoMessages.SetSecretsRequest) 16 | for k, v := range input.Secrets { 17 | s.Secrets = append(s.Secrets, &protoMessages.Secret{ 18 | Key: k, 19 | Value: v, 20 | }) 21 | } 22 | return s 23 | } 24 | 25 | // MakeSetSecretsResponse creates protoMessages.SetSecretsResponse from error 26 | func MakeSetSecretsResponse(err error) *protoMessages.SetSecretsResponse { 27 | s := new(protoMessages.SetSecretsResponse) 28 | if err != nil { 29 | s.Error = &protoMessages.Error{ 30 | Msg: err.Error(), 31 | } 32 | } 33 | return s 34 | } 35 | 36 | // MakeSetSecretsInput creates driver.SetSecretsInput from protoMessages.SetSecretsRequest 37 | func MakeSetSecretsInput(req *protoMessages.SetSecretsRequest) driver.SetSecretsInput { 38 | var in driver.SetSecretsInput 39 | secrets := req.GetSecrets() 40 | if len(secrets) > 0 { 41 | in.Secrets = make(driver.Secrets, len(secrets)) 42 | for _, v := range secrets { 43 | in.Secrets[v.Key] = v.Value 44 | } 45 | } 46 | return in 47 | } 48 | 49 | // MakeSetSecretsOutput creates driver.SetSecretsOutput from protoMessages.SetSecretsResponse 50 | func MakeSetSecretsOutput(resp *protoMessages.SetSecretsResponse) driver.SetSecretsOutput { 51 | var out driver.SetSecretsOutput 52 | if resp.GetError() != nil { 53 | out.Error = &driver.Error{ 54 | Message: resp.GetError().GetMsg(), 55 | } 56 | } 57 | return out 58 | } 59 | 60 | // ReadSetSecretsInput reads io.Reader until io.EOF and returs driver.SetSecretsInput 61 | func ReadSetSecretsInput(r io.Reader) (driver.SetSecretsInput, error) { 62 | var err error 63 | var b []byte 64 | var out driver.SetSecretsInput 65 | protoMsg := new(protoMessages.SetSecretsRequest) 66 | if b, err = ioutil.ReadAll(r); err == nil { 67 | if err = protobuf.Unmarshal(b, protoMsg); err == nil { 68 | out = MakeSetSecretsInput(protoMsg) 69 | } 70 | } 71 | return out, err 72 | } 73 | 74 | // WriteSetSecretsInput writes SetSecretsInput into io.Writer 75 | func WriteSetSecretsInput(w io.Writer, input driver.SetSecretsInput) error { 76 | req := MakeSetSecretsRequest(input) 77 | b, err := protobuf.Marshal(req) 78 | if err == nil { 79 | _, err = w.Write(b) 80 | } 81 | return err 82 | } 83 | 84 | // ReadSetSecretsOutput reads io.Reader until io.EOF and returs driver.SetSecretsOutput 85 | func ReadSetSecretsOutput(r io.Reader) (driver.SetSecretsOutput, error) { 86 | var err error 87 | var b []byte 88 | var out driver.SetSecretsOutput 89 | protoMsg := new(protoMessages.SetSecretsResponse) 90 | if b, err = ioutil.ReadAll(r); err == nil { 91 | if err = protobuf.Unmarshal(b, protoMsg); err == nil { 92 | out = MakeSetSecretsOutput(protoMsg) 93 | } 94 | } 95 | return out, err 96 | } 97 | 98 | // WriteSetSecretsOutput writes SetSecretsOutput into io.Writer 99 | func WriteSetSecretsOutput(w io.Writer, rerr error) error { 100 | req := MakeSetSecretsResponse(rerr) 101 | b, err := protobuf.Marshal(req) 102 | if err == nil { 103 | _, err = w.Write(b) 104 | } 105 | return err 106 | } 107 | -------------------------------------------------------------------------------- /cmd/local/start.go: -------------------------------------------------------------------------------- 1 | // Package localcmd is a local command 2 | /* 3 | Copyright © 2020 NAME HERE 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package localcmd 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "net/http" 23 | 24 | crs "github.com/graphql-editor/stucco/pkg/cors" 25 | "github.com/graphql-editor/stucco/pkg/handlers" 26 | "github.com/graphql-editor/stucco/pkg/server" 27 | "github.com/graphql-editor/stucco/pkg/utils" 28 | "github.com/rs/cors" 29 | "github.com/spf13/cobra" 30 | "k8s.io/apiserver/pkg/server/httplog" 31 | "k8s.io/klog" 32 | ) 33 | 34 | type klogErrorf struct{} 35 | 36 | func (klogErrorf) Errorf(msg string, args ...interface{}) { 37 | klog.Errorf(msg, args...) 38 | } 39 | 40 | // NewStartCommand creates a start command 41 | func NewStartCommand() *cobra.Command { 42 | var startConfig string 43 | var schema string 44 | startCommand := &cobra.Command{ 45 | Use: "start", 46 | Short: "Start local runner", 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | var cfg server.Config 49 | if err := utils.LoadConfigFile(startConfig, &cfg); err != nil { 50 | return err 51 | } 52 | if schema != "" { 53 | cfg.Schema = schema 54 | } 55 | dri := server.NewDefaultDrivers() 56 | if err := dri.Load(); err != nil { 57 | return err 58 | } 59 | defer dri.Close() 60 | h, err := server.New(cfg) 61 | if err != nil { 62 | return err 63 | } 64 | webhookHandler, err := server.NewWebhookHandler(cfg) 65 | if err != nil { 66 | return err 67 | } 68 | corsOptions := crs.NewCors() 69 | fmt.Println(corsOptions.AllowedOrigins) 70 | middleware := func(next http.Handler) http.Handler { 71 | return handlers.RecoveryHandler( 72 | httplog.WithLogging( 73 | cors.New(cors.Options{ 74 | AllowedOrigins: corsOptions.AllowedOrigins, 75 | AllowedMethods: corsOptions.AllowedMethods, 76 | AllowedHeaders: corsOptions.AllowedHeaders, 77 | AllowCredentials: corsOptions.AllowedCredentials, 78 | }).Handler(next), 79 | httplog.DefaultStacktracePred, 80 | ), 81 | klogErrorf{}, 82 | ) 83 | } 84 | h = middleware(h) 85 | webhookHandler = middleware(webhookHandler) 86 | srv := server.Server{ 87 | Handler: h, 88 | WebhookHandler: webhookHandler, 89 | Addr: ":8080", 90 | } 91 | return srv.ListenAndServe() 92 | }, 93 | } 94 | klogFlagSet := flag.NewFlagSet("klog", flag.ExitOnError) 95 | klog.InitFlags(klogFlagSet) 96 | if verb := klogFlagSet.Lookup("v"); verb != nil { 97 | l := klog.Level(3) 98 | verb.DefValue = l.String() 99 | verbosityLevel := (verb.Value.(*klog.Level)) 100 | *verbosityLevel = l 101 | } 102 | startCommand.Flags().AddGoFlagSet(klogFlagSet) 103 | startCommand.Flags().StringVarP(&startConfig, "config", "c", "", "path to stucco config") 104 | startCommand.Flags().StringVarP(&schema, "schema", "s", "", "path to stucco config") 105 | return startCommand 106 | } 107 | -------------------------------------------------------------------------------- /cmd/config/add.go: -------------------------------------------------------------------------------- 1 | package configcmd 2 | 3 | import ( 4 | "github.com/graphql-editor/stucco/pkg/utils" 5 | "github.com/spf13/cobra" 6 | "k8s.io/klog" 7 | ) 8 | 9 | func addCommand() *cobra.Command { 10 | configCommand := &cobra.Command{ 11 | Use: "add", 12 | Short: "add resolver/scalar/etc. to stucco.json/.yaml", 13 | } 14 | configCommand.AddCommand(resolverCommand()) 15 | configCommand.AddCommand(interfaceCommand()) 16 | configCommand.AddCommand(unionCommand()) 17 | configCommand.AddCommand(scalarCommand()) 18 | configCommand.AddCommand(schemaCommand()) 19 | return configCommand 20 | } 21 | 22 | func schemaCommand() *cobra.Command { 23 | addScalarCommand := &cobra.Command{ 24 | Use: "schema", 25 | Short: "Add schema to stucco.json/.yaml [arg1: string]", 26 | Args: cobra.ExactArgs(1), 27 | Run: func(cmd *cobra.Command, args []string) { 28 | cfg, err := addToConfig() 29 | if err != nil { 30 | klog.Fatalln(err.Error()) 31 | } 32 | 33 | cfg.AddSchema(args[0]) 34 | 35 | err = utils.SaveConfigFile("stucco", cfg) 36 | 37 | if err != nil { 38 | klog.Fatalln(err.Error()) 39 | } 40 | }, 41 | } 42 | return addScalarCommand 43 | } 44 | 45 | func scalarCommand() *cobra.Command { 46 | addScalarCommand := &cobra.Command{ 47 | Use: "scalar", 48 | Args: cobra.MinimumNArgs(2), 49 | Short: "Add scalar to stucco.json/.yaml [arg1: Name, arg2: Parse, arg3: Serialize]", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | cfg, err := addToConfig() 52 | if err != nil { 53 | klog.Fatalln(err.Error()) 54 | } 55 | 56 | if len(args) == 2 { 57 | args = append(args, "") 58 | } 59 | 60 | cfg.AddScalar(args[0], args[1], args[2]) 61 | 62 | err = utils.SaveConfigFile("stucco", cfg) 63 | 64 | if err != nil { 65 | klog.Fatalln(err.Error()) 66 | } 67 | }, 68 | } 69 | return addScalarCommand 70 | } 71 | 72 | func unionCommand() *cobra.Command { 73 | addUnionCommand := &cobra.Command{ 74 | Use: "union", 75 | Short: "Add union to stucco.json/.yaml", 76 | Args: cobra.ExactArgs(2), 77 | Run: func(cmd *cobra.Command, args []string) { 78 | cfg, err := addToConfig() 79 | if err != nil { 80 | klog.Fatalln(err.Error()) 81 | } 82 | 83 | cfg.AddUnion(args[0], args[1]) 84 | 85 | err = utils.SaveConfigFile("stucco", cfg) 86 | 87 | if err != nil { 88 | klog.Fatalln(err.Error()) 89 | } 90 | }, 91 | } 92 | return addUnionCommand 93 | } 94 | 95 | func interfaceCommand() *cobra.Command { 96 | addInterfaceCommand := &cobra.Command{ 97 | Use: "interface", 98 | Short: "Add interface to stucco.json/.yaml", 99 | Args: cobra.ExactArgs(2), 100 | Run: func(cmd *cobra.Command, args []string) { 101 | cfg, err := addToConfig() 102 | if err != nil { 103 | klog.Fatalln(err.Error()) 104 | } 105 | 106 | cfg.AddInterface(args[0], args[1]) 107 | 108 | err = utils.SaveConfigFile("stucco", cfg) 109 | 110 | if err != nil { 111 | klog.Fatalln(err.Error()) 112 | } 113 | }, 114 | } 115 | return addInterfaceCommand 116 | } 117 | 118 | func resolverCommand() *cobra.Command { 119 | addResolverCommand := &cobra.Command{ 120 | Use: "resolver", 121 | Short: "Add resolver to stucco.json/.yaml", 122 | Args: cobra.ExactArgs(2), 123 | Run: func(cmd *cobra.Command, args []string) { 124 | cfg, err := addToConfig() 125 | if err != nil { 126 | klog.Fatalln(err.Error()) 127 | } 128 | 129 | cfg.AddResolver(args[0], args[1]) 130 | 131 | err = utils.SaveConfigFile("stucco", cfg) 132 | 133 | if err != nil { 134 | klog.Fatalln(err.Error()) 135 | } 136 | }, 137 | } 138 | return addResolverCommand 139 | } 140 | -------------------------------------------------------------------------------- /pkg/utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // StuccoConfigEnv is a name of environment variable that will be checked for stucco.json path if one is not provided 15 | const StuccoConfigEnv = "STUCCO_CONFIG" 16 | 17 | type decodeFunc func([]byte, interface{}) error 18 | type encodeFunc func(interface{}) ([]byte, error) 19 | 20 | func yamlUnmarshal(b []byte, v interface{}) error { 21 | return yaml.Unmarshal(b, v) 22 | } 23 | 24 | var supportedExtension = map[string]decodeFunc{ 25 | ".json": json.Unmarshal, 26 | ".yaml": yamlUnmarshal, 27 | ".yml": yamlUnmarshal, 28 | } 29 | var supportedExtensionEncode = map[string]encodeFunc{ 30 | ".json": json.Marshal, 31 | ".yaml": yaml.Marshal, 32 | ".yml": yaml.Marshal, 33 | } 34 | 35 | func getConfigExt(fn string) (ext string, isurl bool, err error) { 36 | u, err := url.Parse(fn) 37 | if err != nil { 38 | return 39 | } 40 | for k := range supportedExtension { 41 | if strings.HasSuffix(u.Path, k) { 42 | return k, u.Scheme != "", nil 43 | } 44 | } 45 | if u.Scheme != "" { 46 | err = errors.Errorf("remote config path must be end with extension") 47 | return 48 | } 49 | var st os.FileInfo 50 | for k := range supportedExtension { 51 | st, err = os.Stat(fn + k) 52 | if err == nil || !os.IsNotExist(err) { 53 | ext = k 54 | break 55 | } 56 | } 57 | if err != nil || st.IsDir() { 58 | if os.IsNotExist(err) { 59 | err = errors.Errorf("could not find stucco config at %s", fn) 60 | } 61 | if err == nil { 62 | err = errors.Errorf("%s is a directory", st.Name()) 63 | } 64 | } 65 | return 66 | } 67 | 68 | func realConfigFileName(fn string) (configPath string, err error) { 69 | if fn == "" { 70 | if env := os.Getenv(StuccoConfigEnv); env != "" { 71 | fn = env 72 | } else { 73 | fn = "./stucco" 74 | } 75 | } 76 | ext, isurl, err := getConfigExt(fn) 77 | if err == nil { 78 | configPath = fn 79 | if !isurl && !strings.HasSuffix(fn, ext) { 80 | configPath = fn + ext 81 | } 82 | } 83 | return 84 | } 85 | 86 | // ReadConfigFile loads stucco config from json or yaml file. 87 | // 88 | // If extension is provided function loads config directly, otherwise it tries .json, .yaml and .yml extensions. 89 | func ReadConfigFile(fn string) (b []byte, err error) { 90 | configPath, err := realConfigFileName(fn) 91 | if err == nil { 92 | return ReadLocalOrRemoteFile(configPath) 93 | } 94 | return 95 | } 96 | 97 | // SaveConfigFile saves config to file 98 | func SaveConfigFile(fn string, data interface{}) (err error) { 99 | ext, _, _ := getConfigExt(fn) 100 | configPath, err := realConfigFileName(fn) 101 | if err != nil { 102 | return err 103 | } 104 | encode := supportedExtensionEncode[ext] 105 | 106 | file, err := encode(data) 107 | 108 | if err != nil { 109 | return err 110 | } 111 | 112 | return ioutil.WriteFile(configPath, file, 0644) 113 | } 114 | 115 | // LoadConfigFile returns Config from file 116 | func LoadConfigFile(fn string, v interface{}) (err error) { 117 | configPath, err := realConfigFileName(fn) 118 | var b []byte 119 | if err == nil { 120 | b, err = ReadConfigFile(configPath) 121 | } 122 | if err == nil { 123 | var u *url.URL 124 | u, err = url.Parse(configPath) 125 | if err == nil { 126 | // TODO: Check based on response content-type for remote configs 127 | ext := u.Path[strings.LastIndex(u.Path, "."):] 128 | decode := supportedExtension[ext] 129 | if decode != nil { 130 | err = decode(b, v) 131 | } else { 132 | err = errors.Errorf("%s is not a supported config extension", ext) 133 | } 134 | } 135 | } 136 | return 137 | } 138 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/server.go: -------------------------------------------------------------------------------- 1 | package protohttp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | protobuf "google.golang.org/protobuf/proto" 9 | "github.com/graphql-editor/stucco/pkg/driver" 10 | ) 11 | 12 | // Muxer for Protocol Buffer handler 13 | type Muxer interface { 14 | Authorize(driver.AuthorizeInput) (bool, error) 15 | FieldResolve(driver.FieldResolveInput) (interface{}, error) 16 | InterfaceResolveType(driver.InterfaceResolveTypeInput) (string, error) 17 | SetSecrets(driver.SetSecretsInput) error 18 | ScalarParse(driver.ScalarParseInput) (interface{}, error) 19 | ScalarSerialize(driver.ScalarSerializeInput) (interface{}, error) 20 | UnionResolveType(driver.UnionResolveTypeInput) (string, error) 21 | SubscriptionConnection(driver.SubscriptionConnectionInput) (interface{}, error) 22 | } 23 | 24 | // ErrorLogger logs unrecoverable errors while handling request 25 | type ErrorLogger interface { 26 | Error(err error) 27 | } 28 | 29 | // Handler is a http.Handler for Protocol Buffers server 30 | type Handler struct { 31 | Muxer 32 | ErrorLogger 33 | } 34 | 35 | type httpError interface { 36 | Write(http.ResponseWriter) 37 | } 38 | 39 | type requestError struct { 40 | msg string 41 | args []interface{} 42 | status int 43 | } 44 | 45 | func (e requestError) Write(rw http.ResponseWriter) { 46 | rw.Header().Add(contentTypeHeader, "text/plain") 47 | fmt.Fprintf(rw, e.msg, e.args...) 48 | rw.WriteHeader(e.status) 49 | } 50 | 51 | type badRequest struct { 52 | msg string 53 | args []interface{} 54 | } 55 | 56 | func (e badRequest) Write(rw http.ResponseWriter) { 57 | requestError{ 58 | msg: "BadRequest: " + e.msg, 59 | args: e.args, 60 | status: http.StatusBadRequest, 61 | }.Write(rw) 62 | } 63 | 64 | type internalServerError struct { 65 | msg string 66 | args []interface{} 67 | } 68 | 69 | func (e internalServerError) Write(rw http.ResponseWriter) { 70 | requestError{ 71 | msg: "InternalServerError: " + e.msg, 72 | args: e.args, 73 | status: http.StatusInternalServerError, 74 | }.Write(rw) 75 | } 76 | 77 | func (h *Handler) serveHTTP(req *http.Request, rw http.ResponseWriter) error { 78 | messageType, err := getMessageType(req.Header.Get(contentTypeHeader)) 79 | if err != nil { 80 | br := badRequest{ 81 | msg: "invalid content type: %s", 82 | args: []interface{}{err.Error()}, 83 | } 84 | br.Write(rw) 85 | return nil 86 | } 87 | switch messageType { 88 | case string(authorizeRequestMessage): 89 | err = h.authorize(req, rw) 90 | case string(fieldResolveRequestMessage): 91 | err = h.fieldResolve(req, rw) 92 | case string(interfaceResolveTypeRequestMessage): 93 | err = h.interfaceResolveType(req, rw) 94 | case string(setSecretsRequestMessage): 95 | err = h.setSecrets(req, rw) 96 | case string(scalarParseRequestMessage): 97 | err = h.scalarParse(req, rw) 98 | case string(scalarSerializeRequestMessage): 99 | err = h.scalarSerialize(req, rw) 100 | case string(unionResolveTypeRequestMessage): 101 | err = h.unionResolveType(req, rw) 102 | case string(subscriptionConnectionRequestMessage): 103 | err = h.subscriptionConnection(req, rw) 104 | default: 105 | br := badRequest{ 106 | msg: "invalid content type: %s", 107 | args: []interface{}{err.Error()}, 108 | } 109 | br.Write(rw) 110 | return nil 111 | } 112 | return err 113 | } 114 | 115 | func writeProto(w io.Writer, p protobuf.Message) error { 116 | b, err := protobuf.Marshal(p) 117 | if err == nil { 118 | _, err = w.Write(b) 119 | } 120 | return err 121 | } 122 | 123 | // ServeHTTP implements http.Handler interface for Protocol Buffer server 124 | func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 125 | if err := h.serveHTTP(req, rw); h.ErrorLogger != nil { 126 | h.ErrorLogger.Error(err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /pkg/printer/format.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/logrusorgru/aurora" 9 | "github.com/mattn/go-colorable" 10 | "github.com/mattn/go-isatty" 11 | "github.com/mitchellh/go-wordwrap" 12 | ) 13 | 14 | const ( 15 | wordWrap = 80 16 | ) 17 | 18 | var ( 19 | noteColor = aurora.Blue 20 | highlightColor = aurora.Red 21 | errorColor = aurora.Red 22 | stderr io.Writer = os.Stderr 23 | writer = func() io.Writer { 24 | if f, ok := stderr.(*os.File); ok { 25 | fd := f.Fd() 26 | if isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) { 27 | return colorable.NewColorable(f) 28 | } 29 | } 30 | return colorable.NewNonColorable(stderr) 31 | }() 32 | ) 33 | 34 | // Printf prints formated message to stdout wrapped 35 | func Printf(format string, a ...interface{}) { 36 | fmt.Print(wordwrap.WrapString(fmt.Sprintf(format, a...), wordWrap)) 37 | } 38 | 39 | // Print prints message to stdout wrapped 40 | func Print(a ...interface{}) { 41 | fmt.Print(wordwrap.WrapString(fmt.Sprint(a...), wordWrap)) 42 | } 43 | 44 | // Println prints message to stdout wrapped and follows it with new line 45 | func Println(a ...interface{}) { 46 | fmt.Println(wordwrap.WrapString(fmt.Sprint(a...), wordWrap)) 47 | } 48 | 49 | // ColorPrintf prints formated message to stdout wrapped and colored 50 | func ColorPrintf(format string, args ...interface{}) { 51 | colorPrintf(nil, highlightColor, format, args...) 52 | } 53 | 54 | // ColorPrint prints message to stdout wrapped and colored 55 | func ColorPrint(args ...interface{}) { 56 | colorPrint(nil, args...) 57 | } 58 | 59 | // ColorPrintln prints message to stdout wrapped and colored followed by new line 60 | func ColorPrintln(args ...interface{}) { 61 | colorPrintln(nil, args...) 62 | } 63 | 64 | // NotePrintf prints formated message to stdout wrapped and colored 65 | func NotePrintf(format string, args ...interface{}) { 66 | colorPrintf(noteColor, highlightColor, format, args...) 67 | } 68 | 69 | // NotePrint prints message to stdout wrapped and colored 70 | func NotePrint(args ...interface{}) { 71 | colorPrint(noteColor, args...) 72 | } 73 | 74 | // NotePrintln prints message to stdout wrapped and colored followed by new line 75 | func NotePrintln(args ...interface{}) { 76 | colorPrintln(noteColor, args...) 77 | } 78 | 79 | // ErrorPrintf prints formated message to stdout wrapped and colored 80 | func ErrorPrintf(format string, args ...interface{}) { 81 | colorPrintf(errorColor, nil, format, args...) 82 | } 83 | 84 | // ErrorPrint prints message to stdout wrapped and colored 85 | func ErrorPrint(args ...interface{}) { 86 | colorPrint(errorColor, args...) 87 | } 88 | 89 | // ErrorPrintln prints message to stdout wrapped and colored followed by new line 90 | func ErrorPrintln(args ...interface{}) { 91 | colorPrintln(errorColor, args...) 92 | } 93 | 94 | func colorize(color func(a interface{}) aurora.Value, args ...interface{}) interface{} { 95 | s := fmt.Sprint(args...) 96 | var t interface{} = wordwrap.WrapString(s, wordWrap) 97 | if color != nil { 98 | t = color(t) 99 | } 100 | return t 101 | } 102 | 103 | func colorPrintf( 104 | baseColor func(a interface{}) aurora.Value, 105 | argColor func(a interface{}) aurora.Value, 106 | format string, 107 | args ...interface{}, 108 | ) { 109 | hArgs := []interface{}{} 110 | for _, a := range args { 111 | if argColor != nil { 112 | hArgs = append(hArgs, argColor(a)) 113 | } else { 114 | hArgs = append(hArgs, a) 115 | } 116 | } 117 | var t interface{} = format 118 | if baseColor != nil { 119 | t = baseColor(t) 120 | } 121 | fmt.Print(wordwrap.WrapString( 122 | aurora.Sprintf(t, hArgs...), wordWrap, 123 | )) 124 | } 125 | 126 | func colorPrint(color func(a interface{}) aurora.Value, args ...interface{}) { 127 | fmt.Print(colorize(color, args...)) 128 | } 129 | 130 | func colorPrintln(color func(a interface{}) aurora.Value, args ...interface{}) { 131 | fmt.Println(colorize(color, args...)) 132 | } 133 | -------------------------------------------------------------------------------- /pkg/proto/driver/authorize.go: -------------------------------------------------------------------------------- 1 | package protodriver 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | 7 | protobuf "google.golang.org/protobuf/proto" 8 | "github.com/graphql-editor/stucco/pkg/driver" 9 | "github.com/graphql-editor/stucco/pkg/types" 10 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 11 | ) 12 | 13 | // MakeAuthorizeRequest creates a new proto AuthorizeRequest from driver input 14 | func MakeAuthorizeRequest(input driver.AuthorizeInput) (r *protoMessages.AuthorizeRequest, err error) { 15 | variableValues, err := mapOfAnyToMapOfValue(input.VariableValues) 16 | if err != nil { 17 | return 18 | } 19 | protocol, err := anyToValue(input.Protocol) 20 | if err != nil { 21 | return 22 | } 23 | r = &protoMessages.AuthorizeRequest{ 24 | Function: &protoMessages.Function{ 25 | Name: input.Function.Name, 26 | }, 27 | Query: input.Query, 28 | OperationName: input.OperationName, 29 | VariableValues: variableValues, 30 | Protocol: protocol, 31 | } 32 | return 33 | } 34 | 35 | // MakeAuthorizeOutput creates new driver.AuthorizeOutput from proto response 36 | func MakeAuthorizeOutput(resp *protoMessages.AuthorizeResponse) (out driver.AuthorizeOutput) { 37 | out.Response = resp.GetResponse() 38 | if rerr := resp.GetError(); rerr != nil { 39 | out.Error = &driver.Error{Message: rerr.GetMsg()} 40 | } 41 | return out 42 | } 43 | 44 | // MakeAuthorizeInput creates driver.AuthorizeInput from protoMessages.Authorizerequest 45 | func MakeAuthorizeInput(input *protoMessages.AuthorizeRequest) (f driver.AuthorizeInput, err error) { 46 | variables := input.GetVariableValues() 47 | variableValues, err := mapOfValueToMapOfAny(nil, variables) 48 | if err != nil { 49 | return 50 | } 51 | protocol, err := valueToAny(nil, input.GetProtocol()) 52 | if err != nil { 53 | return 54 | } 55 | f = driver.AuthorizeInput{ 56 | Function: types.Function{ 57 | Name: input.GetFunction().GetName(), 58 | }, 59 | Query: input.GetQuery(), 60 | OperationName: input.GetOperationName(), 61 | VariableValues: variableValues, 62 | Protocol: protocol, 63 | } 64 | return 65 | } 66 | 67 | // MakeAuthorizeResponse creates a protoMessages.AuthorizeResponse from a value 68 | func MakeAuthorizeResponse(resp bool) *protoMessages.AuthorizeResponse { 69 | return &protoMessages.AuthorizeResponse{ 70 | Response: resp, 71 | } 72 | } 73 | 74 | // ReadAuthorizeInput reads io.Reader until io.EOF and returs driver.AuthorizeInput 75 | func ReadAuthorizeInput(r io.Reader) (driver.AuthorizeInput, error) { 76 | var err error 77 | var b []byte 78 | var out driver.AuthorizeInput 79 | protoMsg := new(protoMessages.AuthorizeRequest) 80 | if b, err = ioutil.ReadAll(r); err == nil { 81 | if err = protobuf.Unmarshal(b, protoMsg); err == nil { 82 | out, err = MakeAuthorizeInput(protoMsg) 83 | } 84 | } 85 | return out, err 86 | } 87 | 88 | // WriteAuthorizeInput writes AuthorizeInput into io.Writer 89 | func WriteAuthorizeInput(w io.Writer, input driver.AuthorizeInput) error { 90 | req, err := MakeAuthorizeRequest(input) 91 | if err == nil { 92 | var b []byte 93 | b, err = protobuf.Marshal(req) 94 | if err == nil { 95 | _, err = w.Write(b) 96 | } 97 | } 98 | return err 99 | } 100 | 101 | // ReadAuthorizeOutput reads io.Reader until io.EOF and returs driver.AuthorizeOutput 102 | func ReadAuthorizeOutput(r io.Reader) (driver.AuthorizeOutput, error) { 103 | var err error 104 | var b []byte 105 | var out driver.AuthorizeOutput 106 | protoMsg := new(protoMessages.AuthorizeResponse) 107 | if b, err = ioutil.ReadAll(r); err == nil { 108 | if err = protobuf.Unmarshal(b, protoMsg); err == nil { 109 | out = MakeAuthorizeOutput(protoMsg) 110 | } 111 | } 112 | return out, err 113 | } 114 | 115 | // WriteAuthorizeOutput writes AuthorizeOutput into io.Writer 116 | func WriteAuthorizeOutput(w io.Writer, r bool) error { 117 | req := MakeAuthorizeResponse(r) 118 | b, err := protobuf.Marshal(req) 119 | if err == nil { 120 | _, err = w.Write(b) 121 | } 122 | return err 123 | } 124 | -------------------------------------------------------------------------------- /pkg/grpc/scalar.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/graphql-editor/stucco/pkg/driver" 8 | protodriver "github.com/graphql-editor/stucco/pkg/proto/driver" 9 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 10 | ) 11 | 12 | // ScalarParse executes server side ScalarParse rpc 13 | func (m *Client) ScalarParse(input driver.ScalarParseInput) (s driver.ScalarParseOutput) { 14 | req, err := protodriver.MakeScalarParseRequest(input) 15 | if err == nil { 16 | var resp *protoMessages.ScalarParseResponse 17 | resp, err = m.Client.ScalarParse(context.Background(), req) 18 | if err == nil { 19 | s = protodriver.MakeScalarParseOutput(resp) 20 | } 21 | } 22 | if err != nil { 23 | s.Error = &driver.Error{Message: err.Error()} 24 | } 25 | return 26 | } 27 | 28 | // ScalarSerialize executes server side ScalarSerialize rpc 29 | func (m *Client) ScalarSerialize(input driver.ScalarSerializeInput) (s driver.ScalarSerializeOutput) { 30 | req, err := protodriver.MakeScalarSerializeRequest(input) 31 | if err == nil { 32 | var resp *protoMessages.ScalarSerializeResponse 33 | resp, err = m.Client.ScalarSerialize(context.Background(), req) 34 | if err == nil { 35 | s = protodriver.MakeScalarSerializeOutput(resp) 36 | } 37 | } 38 | if err != nil { 39 | s.Error = &driver.Error{Message: err.Error()} 40 | err = nil 41 | } 42 | return 43 | } 44 | 45 | // ScalarParseHandler interface that must be implemented by user to handle scalar parse 46 | // requests 47 | type ScalarParseHandler interface { 48 | // Handle takes ScalarParseInput as input returning arbitrary parsed value 49 | Handle(driver.ScalarParseInput) (interface{}, error) 50 | } 51 | 52 | // ScalarParseHandlerFunc is a convienience wrapper for function implementing ScalarParseHandler 53 | type ScalarParseHandlerFunc func(driver.ScalarParseInput) (interface{}, error) 54 | 55 | // Handle implements ScalarParseHandler.Handle 56 | func (f ScalarParseHandlerFunc) Handle(input driver.ScalarParseInput) (interface{}, error) { 57 | return f(input) 58 | } 59 | 60 | // ScalarParse calls user defined function for parsing a scalar. 61 | func (m *Server) ScalarParse(ctx context.Context, input *protoMessages.ScalarParseRequest) (s *protoMessages.ScalarParseResponse, _ error) { 62 | defer func() { 63 | if r := recover(); r != nil { 64 | s = &protoMessages.ScalarParseResponse{ 65 | Error: &protoMessages.Error{ 66 | Msg: fmt.Sprintf("%v", r), 67 | }, 68 | } 69 | } 70 | }() 71 | s = new(protoMessages.ScalarParseResponse) 72 | v, err := protodriver.MakeScalarParseInput(input) 73 | if err == nil { 74 | var resp interface{} 75 | resp, err = m.ScalarParseHandler.Handle(v) 76 | if err == nil { 77 | s = protodriver.MakeScalarParseResponse(resp) 78 | } 79 | } 80 | if err != nil { 81 | s = &protoMessages.ScalarParseResponse{ 82 | Error: &protoMessages.Error{Msg: err.Error()}, 83 | } 84 | } 85 | return 86 | } 87 | 88 | // ScalarSerializeHandler interface that must be implemented by user to handle scalar serialize 89 | // requests 90 | type ScalarSerializeHandler interface { 91 | // Handle takes ScalarSerializeInput as input returning arbitrary serialized value 92 | Handle(driver.ScalarSerializeInput) (interface{}, error) 93 | } 94 | 95 | // ScalarSerializeHandlerFunc is a convienience wrapper for function implementing ScalarSerializeHandler 96 | type ScalarSerializeHandlerFunc func(driver.ScalarSerializeInput) (interface{}, error) 97 | 98 | // Handle implements ScalarSerializeHandler.Handle 99 | func (f ScalarSerializeHandlerFunc) Handle(input driver.ScalarSerializeInput) (interface{}, error) { 100 | return f(input) 101 | } 102 | 103 | // ScalarSerialize executes user handler for scalar serialization 104 | func (m *Server) ScalarSerialize(ctx context.Context, input *protoMessages.ScalarSerializeRequest) (s *protoMessages.ScalarSerializeResponse, _ error) { 105 | defer func() { 106 | if r := recover(); r != nil { 107 | s = &protoMessages.ScalarSerializeResponse{ 108 | Error: &protoMessages.Error{ 109 | Msg: fmt.Sprintf("%v", r), 110 | }, 111 | } 112 | } 113 | }() 114 | val, err := protodriver.MakeScalarSerializeInput(input) 115 | if err == nil { 116 | var resp interface{} 117 | resp, err = m.ScalarSerializeHandler.Handle(val) 118 | if err == nil { 119 | s = protodriver.MakeScalarSerializeResponse(resp) 120 | } 121 | } 122 | if err != nil { 123 | s = &protoMessages.ScalarSerializeResponse{ 124 | Error: &protoMessages.Error{Msg: err.Error()}, 125 | } 126 | } 127 | return 128 | } 129 | -------------------------------------------------------------------------------- /cmd/azure/zip_router.go: -------------------------------------------------------------------------------- 1 | package azurecmd 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/graphql-editor/stucco/pkg/providers/azure/project" 12 | "github.com/graphql-editor/stucco/pkg/providers/azure/vars" 13 | "github.com/graphql-editor/stucco/pkg/utils" 14 | global_vars "github.com/graphql-editor/stucco/pkg/vars" 15 | "github.com/spf13/cobra" 16 | "k8s.io/klog" 17 | ) 18 | 19 | func webhookJSON(whPattern string) io.Reader { 20 | return strings.NewReader(`{ 21 | "bindings": [ 22 | { 23 | "authLevel": "Anonymous", 24 | "type": "httpTrigger", 25 | "direction": "in", 26 | "name": "req", 27 | "methods": ["get", "post"], 28 | "route": "` + whPattern + `" 29 | }, 30 | { 31 | "type": "http", 32 | "direction": "out", 33 | "name": "res" 34 | } 35 | ] 36 | }`) 37 | } 38 | 39 | // NewZipRouterCommand returns new zip-router command 40 | func NewZipRouterCommand() *cobra.Command { 41 | var config string 42 | var schema string 43 | var cert string 44 | var key string 45 | var output string 46 | var insecure bool 47 | var ver string 48 | var host string 49 | zipRouter := &cobra.Command{ 50 | Use: "zip-router", 51 | Short: "Create router function zip that can be used in azcli to deploy function", 52 | Run: func(cmd *cobra.Command, args []string) { 53 | configData, err := utils.ReadLocalOrRemoteFile(config) 54 | if err != nil { 55 | klog.Fatal(err) 56 | } 57 | schemaData, err := utils.ReadLocalOrRemoteFile(schema) 58 | if err != nil { 59 | klog.Fatal(err) 60 | } 61 | keyData, err := utils.ReadLocalOrRemoteFile(key) 62 | if err != nil && !insecure { 63 | klog.Fatal(err) 64 | } 65 | certData, err := utils.ReadLocalOrRemoteFile(cert) 66 | if err != nil && !insecure { 67 | klog.Fatal(err) 68 | } 69 | var cfg project.Config 70 | if err := utils.LoadConfigFile(config, &cfg); err != nil { 71 | klog.Fatal(err) 72 | } 73 | extraFiles := []utils.ZipData{ 74 | {Filename: "stucco.json", Data: bytes.NewReader(configData)}, 75 | {Filename: "schema.graphql", Data: bytes.NewReader(schemaData)}, 76 | } 77 | for i, wh := range cfg.AzureOpts.Webhooks { 78 | extraFiles = append(extraFiles, utils.ZipData{ 79 | Filename: "webhook" + strconv.FormatInt(int64(i), 10) + "/function.json", 80 | Data: webhookJSON(wh), 81 | }) 82 | } 83 | if keyData != nil { 84 | extraFiles = append(extraFiles, utils.ZipData{Filename: "key.pem", Data: bytes.NewReader(keyData)}) 85 | } 86 | if certData != nil { 87 | extraFiles = append(extraFiles, utils.ZipData{Filename: "cert.pem", Data: bytes.NewReader(certData)}) 88 | } 89 | var r project.Router 90 | if ver != "" || host != "" { 91 | r.Vars = &vars.Vars{ 92 | Vars: global_vars.Vars{ 93 | Relase: global_vars.Release{ 94 | Version: ver, 95 | Host: host, 96 | }, 97 | }, 98 | } 99 | } 100 | rc, err := r.Zip(extraFiles) 101 | if err != nil { 102 | klog.Fatal(err) 103 | } 104 | defer rc.Close() 105 | d := filepath.Dir(output) 106 | if d != "" { 107 | err = os.MkdirAll(d, 0755) 108 | if err != nil { 109 | klog.Fatal(err) 110 | } 111 | } 112 | f, err := os.Create(output) 113 | if err != nil { 114 | klog.Fatal(err) 115 | } 116 | defer f.Close() 117 | _, err = io.Copy(f, rc) 118 | if err != nil { 119 | klog.Fatal(err) 120 | } 121 | }, 122 | } 123 | defaultConfig := os.Getenv("STUCCO_CONFIG") 124 | if defaultConfig == "" { 125 | defaultConfig = "./stucco.json" 126 | } 127 | defaultSchema := os.Getenv("STUCCO_SCHEMA") 128 | if defaultSchema == "" { 129 | defaultSchema = "./schema.graphql" 130 | } 131 | zipRouter.Flags().StringVarP(&config, "config", "c", defaultConfig, "Path or url to stucco config") 132 | zipRouter.Flags().StringVarP(&schema, "schema", "s", defaultSchema, "Path or url to stucco schema") 133 | zipRouter.Flags().StringVar(&key, "key", "key.pem", "key used in http client cert authentication") 134 | zipRouter.Flags().StringVar(&cert, "cert", "cert.pem", "cert used in http client cert authentication") 135 | zipRouter.Flags().StringVarP(&output, "out", "o", "dist/router.zip", "Router function archive output") 136 | zipRouter.Flags().StringVar(&ver, "zip-version", "", "Use specific version of zip as a base") 137 | zipRouter.Flags().StringVar(&host, "zip-host", "", "Override router base zip host") 138 | zipRouter.Flags().BoolVarP(&insecure, "insecure", "i", false, "Allow zip without certificate files") 139 | return zipRouter 140 | } 141 | -------------------------------------------------------------------------------- /pkg/providers/azure/driver/driver_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/graphql-editor/stucco/pkg/driver" 10 | "github.com/graphql-editor/stucco/pkg/driver/drivertest" 11 | azuredriver "github.com/graphql-editor/stucco/pkg/providers/azure/driver" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | type mockHTTPClient struct { 17 | mock.Mock 18 | } 19 | 20 | func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { 21 | called := m.Called(req) 22 | if resp, ok := called.Get(0).(*http.Response); ok { 23 | return resp, called.Error(1) 24 | } 25 | return nil, called.Error(1) 26 | } 27 | 28 | func TestProtobufNewClient(t *testing.T) { 29 | assert.NotNil(t, azuredriver.ProtobufClient{}.New("http://mockurl", "funcname")) 30 | assert.NotNil(t, azuredriver.ProtobufClient{ 31 | HTTPClient: http.DefaultClient, 32 | }.New("http://mockurl", "funcname")) 33 | var mockHTTPClient mockHTTPClient 34 | mockHTTPClient.On("Do", mock.MatchedBy(func(v interface{}) bool { 35 | req, ok := v.(*http.Request) 36 | return ok && "http://mockurl" == req.URL.String() 37 | })).Return(nil, nil) 38 | azuredriver.ProtobufClient{ 39 | HTTPClient: &mockHTTPClient, 40 | }.Post("http://mockurl", "some/content", nil) 41 | } 42 | 43 | type mockWorkerClient struct { 44 | mock.Mock 45 | } 46 | 47 | func (m *mockWorkerClient) New(a, b string) driver.Driver { 48 | return m.Called(a, b).Get(0).(driver.Driver) 49 | } 50 | 51 | func TestDriver(t *testing.T) { 52 | os.Setenv("STUCCO_AZURE_WORKER_BASE_URL", "http://mockurl") 53 | defer os.Unsetenv("STUCCO_AZURE_WORKER_BASE_URL") 54 | var mockDriver drivertest.MockDriver 55 | var mockWorkerClient mockWorkerClient 56 | mockWorkerClient.On("New", mock.MatchedBy(func(v interface{}) bool { 57 | m, ok := v.(string) 58 | return ok && strings.HasPrefix(m, "http://mockurl") 59 | }), "").Return(&mockDriver) 60 | d := azuredriver.Driver{ 61 | WorkerClient: &mockWorkerClient, 62 | } 63 | 64 | // Test FieldResolve 65 | mockDriver.On("FieldResolve", driver.FieldResolveInput{}).Return(driver.FieldResolveOutput{}) 66 | assert.Equal(t, driver.FieldResolveOutput{}, d.FieldResolve(driver.FieldResolveInput{})) 67 | mockDriver.AssertCalled(t, "FieldResolve", driver.FieldResolveInput{}) 68 | 69 | // Test InterfaceResolveType 70 | mockDriver.On("InterfaceResolveType", driver.InterfaceResolveTypeInput{}).Return(driver.InterfaceResolveTypeOutput{}) 71 | assert.Equal(t, driver.InterfaceResolveTypeOutput{}, d.InterfaceResolveType(driver.InterfaceResolveTypeInput{})) 72 | mockDriver.AssertCalled(t, "InterfaceResolveType", driver.InterfaceResolveTypeInput{}) 73 | 74 | // Test ScalarParse 75 | mockDriver.On("ScalarParse", driver.ScalarParseInput{}).Return(driver.ScalarParseOutput{}) 76 | assert.Equal(t, driver.ScalarParseOutput{}, d.ScalarParse(driver.ScalarParseInput{})) 77 | mockDriver.AssertCalled(t, "ScalarParse", driver.ScalarParseInput{}) 78 | 79 | // Test ScalarSerialize 80 | mockDriver.On("ScalarSerialize", driver.ScalarSerializeInput{}).Return(driver.ScalarSerializeOutput{}) 81 | assert.Equal(t, driver.ScalarSerializeOutput{}, d.ScalarSerialize(driver.ScalarSerializeInput{})) 82 | mockDriver.AssertCalled(t, "ScalarSerialize", driver.ScalarSerializeInput{}) 83 | 84 | // Test UnionResolveType 85 | mockDriver.On("UnionResolveType", driver.UnionResolveTypeInput{}).Return(driver.UnionResolveTypeOutput{}) 86 | assert.Equal(t, driver.UnionResolveTypeOutput{}, d.UnionResolveType(driver.UnionResolveTypeInput{})) 87 | mockDriver.AssertCalled(t, "UnionResolveType", driver.UnionResolveTypeInput{}) 88 | 89 | // Test Stream 90 | mockDriver.On("Stream", driver.StreamInput{}).Return(driver.StreamOutput{}) 91 | assert.Equal(t, driver.StreamOutput{}, d.Stream(driver.StreamInput{})) 92 | mockDriver.AssertCalled(t, "Stream", driver.StreamInput{}) 93 | 94 | os.Setenv("STUCCO_AZURE_WORKER_BASE_URL", "://mockurl") 95 | 96 | // Test FieldResolve 97 | f := d.FieldResolve(driver.FieldResolveInput{}) 98 | assert.NotNil(t, f.Error) 99 | 100 | // Test InterfaceResolveType 101 | i := d.InterfaceResolveType(driver.InterfaceResolveTypeInput{}) 102 | assert.NotNil(t, i.Error) 103 | 104 | // Test ScalarParse 105 | sp := d.ScalarParse(driver.ScalarParseInput{}) 106 | assert.NotNil(t, sp.Error) 107 | 108 | // Test ScalarSerialize 109 | ss := d.ScalarSerialize(driver.ScalarSerializeInput{}) 110 | assert.NotNil(t, ss.Error) 111 | 112 | // Test UnionResolveType 113 | u := d.UnionResolveType(driver.UnionResolveTypeInput{}) 114 | assert.NotNil(t, u.Error) 115 | 116 | // Test Stream 117 | s := d.Stream(driver.StreamInput{}) 118 | assert.NotNil(t, s.Error) 119 | 120 | mockWorkerClient.AssertNumberOfCalls(t, "New", 6) 121 | } 122 | -------------------------------------------------------------------------------- /pkg/driver/protohttp/scalar_test.go: -------------------------------------------------------------------------------- 1 | package protohttp_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | protobuf "google.golang.org/protobuf/proto" 11 | "github.com/graphql-editor/stucco/pkg/driver/protohttp" 12 | "github.com/graphql-editor/stucco/pkg/proto/prototest" 13 | protoMessages "github.com/graphql-editor/stucco_proto/go/messages" 14 | "github.com/stretchr/testify/assert" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | func TestClientScalarParse(t *testing.T) { 19 | prototest.RunScalarParseClientTests(t, func(t *testing.T, tt prototest.ScalarParseClientTest) { 20 | srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 21 | if tt.ProtoError != nil { 22 | rw.WriteHeader(http.StatusInternalServerError) 23 | rw.Write([]byte(tt.ProtoError.Error())) 24 | return 25 | } 26 | header := req.Header.Get("content-type") 27 | assert.Equal(t, "application/x-protobuf; message=ScalarParseRequest", header) 28 | body, err := ioutil.ReadAll(req.Body) 29 | assert.NoError(t, err) 30 | req.Body.Close() 31 | var p protoMessages.ScalarParseRequest 32 | assert.NoError(t, proto.Unmarshal(body, &p)) 33 | rw.Header().Add("content-type", "application/x-protobuf; message=ScalarParseResponse") 34 | b, _ := protobuf.Marshal(tt.ProtoResponse) 35 | rw.Write(b) 36 | })) 37 | defer srv.Close() 38 | client := protohttp.NewClient(protohttp.Config{ 39 | Client: srv.Client(), 40 | URL: srv.URL, 41 | }) 42 | out := client.ScalarParse(tt.Input) 43 | assert.Equal(t, tt.Expected, out) 44 | }) 45 | } 46 | 47 | func TestClientScalarSerialize(t *testing.T) { 48 | prototest.RunScalarSerializeClientTests(t, func(t *testing.T, tt prototest.ScalarSerializeClientTest) { 49 | srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 50 | if tt.ProtoError != nil { 51 | rw.WriteHeader(http.StatusInternalServerError) 52 | rw.Write([]byte(tt.ProtoError.Error())) 53 | return 54 | } 55 | header := req.Header.Get("content-type") 56 | assert.Equal(t, "application/x-protobuf; message=ScalarSerializeRequest", header) 57 | body, err := ioutil.ReadAll(req.Body) 58 | assert.NoError(t, err) 59 | req.Body.Close() 60 | var p protoMessages.ScalarParseRequest 61 | assert.NoError(t, proto.Unmarshal(body, &p)) 62 | rw.Header().Add("content-type", "application/x-protobuf; message=ScalarSerializeResponse") 63 | b, _ := protobuf.Marshal(tt.ProtoResponse) 64 | rw.Write(b) 65 | })) 66 | defer srv.Close() 67 | client := protohttp.NewClient(protohttp.Config{ 68 | Client: srv.Client(), 69 | URL: srv.URL, 70 | }) 71 | out := client.ScalarSerialize(tt.Input) 72 | assert.Equal(t, tt.Expected, out) 73 | }) 74 | } 75 | 76 | func TestServerScalarParse(t *testing.T) { 77 | prototest.RunScalarParseServerTests(t, func(t *testing.T, tt prototest.ScalarParseServerTest) { 78 | var r http.Request 79 | b, _ := protobuf.Marshal(tt.Input) 80 | r.Header = make(http.Header) 81 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 82 | r.Header.Add("content-type", "application/x-protobuf; message=ScalarParseRequest") 83 | responseRecorder := httptest.NewRecorder() 84 | mockMuxer := new(mockMuxer) 85 | handler := &protohttp.Handler{ 86 | Muxer: mockMuxer, 87 | } 88 | mockMuxer.On("ScalarParse", tt.HandlerInput).Return(tt.HandlerOutput, tt.HandlerError) 89 | handler.ServeHTTP(responseRecorder, &r) 90 | mockMuxer.AssertCalled(t, "ScalarParse", tt.HandlerInput) 91 | assert.Equal(t, "application/x-protobuf; message=ScalarParseResponse", responseRecorder.Header().Get("content-type")) 92 | var protoResp protoMessages.ScalarParseResponse 93 | assert.NoError(t, protobuf.Unmarshal(responseRecorder.Body.Bytes(), &protoResp)) 94 | }) 95 | } 96 | 97 | func TestServerScalarSerialize(t *testing.T) { 98 | prototest.RunScalarSerializeServerTests(t, func(t *testing.T, tt prototest.ScalarSerializeServerTest) { 99 | var r http.Request 100 | b, _ := protobuf.Marshal(tt.Input) 101 | r.Header = make(http.Header) 102 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 103 | r.Header.Add("content-type", "application/x-protobuf; message=ScalarSerializeRequest") 104 | responseRecorder := httptest.NewRecorder() 105 | mockMuxer := new(mockMuxer) 106 | handler := &protohttp.Handler{ 107 | Muxer: mockMuxer, 108 | } 109 | mockMuxer.On("ScalarSerialize", tt.HandlerInput).Return(tt.HandlerOutput, tt.HandlerError) 110 | handler.ServeHTTP(responseRecorder, &r) 111 | mockMuxer.AssertCalled(t, "ScalarSerialize", tt.HandlerInput) 112 | assert.Equal(t, "application/x-protobuf; message=ScalarSerializeResponse", responseRecorder.Header().Get("content-type")) 113 | var protoResp protoMessages.ScalarSerializeResponse 114 | assert.NoError(t, protobuf.Unmarshal(responseRecorder.Body.Bytes(), &protoResp)) 115 | }) 116 | } 117 | --------------------------------------------------------------------------------