diff --git a/Makefile b/Makefile index f6bf9fc..22a524d 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ typegen: format: gofmt -w -s . +.PHONY: test +test: + go test -v -race -timeout 3m ./... # Install golangci-lint tool to run lint locally # https://golangci-lint.run/usage/install diff --git a/codegen/connector.go b/codegen/connector.go deleted file mode 100644 index 48b7b88..0000000 --- a/codegen/connector.go +++ /dev/null @@ -1,192 +0,0 @@ -package main - -import ( - "fmt" - "go/ast" - "log" - "regexp" - "strings" - - "github.com/hasura/ndc-sdk-go/schema" -) - -var ( - operationNameCommentRegex = regexp.MustCompile(`^@(function|procedure)(\s+([a-z][a-zA-Z0-9_]*))?`) -) - -type OperationKind string - -var ( - OperationFunction OperationKind = "function" - OperationProcedure OperationKind = "procedure" -) - -// FunctionInfo represents a readable Go function info -// which can convert to a NDC function or procedure schema -type OperationInfo struct { - Kind OperationKind - Name string - Description string - Parameters []*ast.Field - ResultType schema.TypeEncoder -} - -// FunctionInfo represents a readable Go function info -// which can convert to a NDC function schema -type FunctionInfo OperationInfo - -// Schema returns a NDC function schema -func (op FunctionInfo) Schema() schema.FunctionInfo { - result := schema.FunctionInfo{ - Name: op.Name, - ResultType: op.ResultType.Encode(), - Arguments: schema.FunctionInfoArguments{}, - } - if op.Description != "" { - result.Description = &op.Description - } - return result -} - -// ProcedureInfo represents a readable Go function info -// which can convert to a NDC procedure schema -type ProcedureInfo FunctionInfo - -// Schema returns a NDC procedure schema -func (op ProcedureInfo) Schema() schema.ProcedureInfo { - result := schema.ProcedureInfo{ - Name: op.Name, - ResultType: op.ResultType.Encode(), - Arguments: schema.ProcedureInfoArguments{}, - } - if op.Description != "" { - result.Description = &op.Description - } - return result -} - -// RawConnectorSchema represents a readable Go schema object -// which can encode to NDC schema -type RawConnectorSchema struct { - Scalars schema.SchemaResponseScalarTypes - Objects []*ast.Object - Functions []FunctionInfo - Procedures []ProcedureInfo -} - -// Schema converts to a NDC schema -func (rcs RawConnectorSchema) Schema() *schema.SchemaResponse { - result := &schema.SchemaResponse{ - ScalarTypes: rcs.Scalars, - ObjectTypes: schema.SchemaResponseObjectTypes{}, - Collections: []schema.CollectionInfo{}, - } - for _, function := range rcs.Functions { - result.Functions = append(result.Functions, function.Schema()) - } - for _, procedure := range rcs.Procedures { - result.Procedures = append(result.Procedures, procedure.Schema()) - } - - return result -} - -// parse raw connector schema from Go code -func parseRawConnectorSchema(packages map[string]*ast.Package) (*RawConnectorSchema, error) { - functions := []FunctionInfo{} - procedures := []ProcedureInfo{} - objectTypes := []*ast.Object{} - - for _, pkg := range packages { - for fileName, file := range pkg.Files { - if file.Scope == nil { - log.Printf("%s: empty scope", fileName) - continue - } - for objName, obj := range file.Scope.Objects { - switch obj.Kind { - case ast.Typ: - parseTypeDeclaration(obj) - objectTypes = append(objectTypes, obj) - case ast.Fun: - if objName == "main" { - continue - } - fnDecl, ok := obj.Decl.(*ast.FuncDecl) - if !ok { - continue - } - if fnDecl.Doc == nil { - continue - } - - opInfo := parseOperationInfoFromComment(objName, fnDecl.Doc.List) - if opInfo == nil { - continue - } - - opInfo.Parameters = fnDecl.Type.Params.List - if len(fnDecl.Type.Results.List) != 2 { - return nil, fmt.Errorf("%s: invalid return result types, expect 2 results (, error)", objName) - } - - resultType, err := getInnerReturnExprType(fnDecl.Type.Results.List[0].Type, true) - if err != nil { - return nil, err - } - opInfo.ResultType = resultType - switch opInfo.Kind { - case OperationFunction: - functions = append(functions, FunctionInfo(*opInfo)) - case OperationProcedure: - procedures = append(procedures, ProcedureInfo(*opInfo)) - } - } - } - } - } - - return &RawConnectorSchema{ - Functions: functions, - Procedures: procedures, - }, nil -} - -func parseTypeDeclaration(object *ast.Object) (map[string]schema.ObjectType, map[string]schema.ScalarType) { - spec, ok := object.Decl.(*ast.TypeSpec) - if !ok { - return nil, nil - } - log.Printf("name: %s, type: %+v", object.Name, spec.Type) - - var comment string - if spec.Comment != nil { - comment = strings.TrimSpace(spec.Comment.Text()) - } - - switch t := spec.Type.(type) { - case *ast.StructType: - log.Printf("obj: %+v", t.Fields) - if t.Fields == nil || len(t.Fields.List) == 0 { - return nil, nil - } - objType := schema.ObjectType{} - if comment != "" { - objType.Description = &comment - } - for _, field := range t.Fields.List { - log.Printf("field name: %+v, doc: %+v, comment: %+v, type: %+v", field.Names, field.Doc, field.Comment, field.Type) - } - return nil, nil - default: - return nil, nil - } -} - -func mergeMap[K comparable, V any](dest map[K]V, src map[K]V) map[K]V { - result := dest - for k, v := range src { - result[k] = v - } - return result -} diff --git a/codegen/main.go b/codegen/main.go deleted file mode 100644 index edde5cc..0000000 --- a/codegen/main.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "go/parser" - "go/token" - "io/fs" - "log" - "strings" -) - -func main() { - filePath := flag.String("path", "", "Source file path") - flag.Parse() - fset := token.NewFileSet() - - pkgs, err := parser.ParseDir(fset, *filePath, func(fi fs.FileInfo) bool { - return !fi.IsDir() && !strings.Contains(fi.Name(), "generated") - }, parser.ParseComments) - if err != nil { - panic(err) - } - sm, err := parseRawConnectorSchema(pkgs) - if err != nil { - panic(err) - } - - bytes, err := json.MarshalIndent(sm.Schema(), "", " ") - if err != nil { - panic(err) - } - - log.Println(string(bytes)) -} diff --git a/codegen/schema.go b/codegen/schema.go deleted file mode 100644 index 3741e29..0000000 --- a/codegen/schema.go +++ /dev/null @@ -1,127 +0,0 @@ -package main - -import ( - "fmt" - "go/ast" - "log" - "strings" - - "github.com/hasura/ndc-sdk-go/schema" -) - -var defaultScalarTypes = schema.SchemaResponseScalarTypes{ - "String": schema.ScalarType{ - AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, - ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{}, - }, - "Int": schema.ScalarType{ - AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, - ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{}, - }, - "Float": schema.ScalarType{ - AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, - ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{}, - }, - "Boolean": schema.ScalarType{ - AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, - ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{}, - }, -} - -// get scalar type name go native type name -func getScalarTypeNameFromNativeTypeName(name string) (string, bool) { - switch name { - case "bool": - return "Boolean", true - case "string": - return "String", true - case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "byte", "rune": - return "Int", true - case "float32", "float64", "complex64", "complex128": - return "Float", true - default: - return "", false - } -} - -// check if the input type name is a scalar -func isTypeNameScalar(input string) bool { - _, ok := getScalarTypeNameFromNativeTypeName(input) - return ok -} - -// get scalar type name go native type -func getScalarTypeNameFromNativeType(input any) (string, bool) { - switch input.(type) { - case bool, *bool: - return "Boolean", true - case string, *string: - return "String", true - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64, *uintptr: - return "Int", true - case float32, float64, complex64, complex128, *float32, *float64, *complex64, *complex128: - return "Float", true - default: - return "", false - } -} - -// check if the input type is a scalar -func isScalar(input any) bool { - _, ok := getScalarTypeNameFromNativeType(input) - return ok -} - -func getInnerReturnExprType(ty ast.Expr, skipNullable bool) (schema.TypeEncoder, error) { - switch t := ty.(type) { - case *ast.StarExpr: - if !skipNullable { - innerType, err := getInnerReturnExprType(t.X, false) - if err != nil { - return nil, err - } - return schema.NewNullableType(innerType), nil - } - return getInnerReturnExprType(t.X, false) - case *ast.Ident: - log.Printf("obj: %+v", *t.Obj) - return schema.NewNamedType(t.Name), nil - default: - return nil, fmt.Errorf("unhandled expr %+v: %+v", ty, t) - } -} - -func parseOperationInfoFromComment(functionName string, comments []*ast.Comment) *OperationInfo { - var result OperationInfo - var descriptions []string - for _, comment := range comments { - text := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) - matches := operationNameCommentRegex.FindStringSubmatch(text) - matchesLen := len(matches) - if matchesLen > 1 { - switch matches[1] { - case string(OperationFunction): - result.Kind = OperationFunction - case string(OperationProcedure): - result.Kind = OperationProcedure - default: - log.Println("unsupported operation kind:", matches[0]) - } - - if matchesLen > 3 && strings.TrimSpace(matches[3]) != "" { - result.Name = strings.TrimSpace(matches[3]) - } else { - result.Name = strings.ToLower(functionName[:1]) + functionName[1:] - } - } else { - descriptions = append(descriptions, text) - } - } - - if result.Kind == "" { - return nil - } - - result.Description = strings.TrimSpace(strings.Join(descriptions, " ")) - return &result -} diff --git a/connector/configuration_test.go b/connector/configuration_test.go index 5cd0ccc..039ce09 100644 --- a/connector/configuration_test.go +++ b/connector/configuration_test.go @@ -36,7 +36,7 @@ func TestConfigurationServer(t *testing.T) { t.Errorf("GET /: expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "GET /", res, http.StatusOK, mockRawConfiguration{ + assertHTTPResponse(t, res, http.StatusOK, mockRawConfiguration{ Version: "1", }) }) @@ -49,7 +49,7 @@ func TestConfigurationServer(t *testing.T) { t.Errorf("POST /: expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /", res, http.StatusOK, mockRawConfiguration{ + assertHTTPResponse(t, res, http.StatusOK, mockRawConfiguration{ Version: "1", }) }) @@ -60,7 +60,7 @@ func TestConfigurationServer(t *testing.T) { t.Errorf("POST /: expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /", res, http.StatusBadRequest, schema.ErrorResponse{ + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ Message: "failed to decode json request body", Details: map[string]string{ "cause": "json: cannot unmarshal string into Go value of type connector.mockRawConfiguration", @@ -74,7 +74,7 @@ func TestConfigurationServer(t *testing.T) { t.Errorf("GET /schema: expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "GET /schema", res, http.StatusOK, jsonschema.Schema{ + assertHTTPResponse(t, res, http.StatusOK, jsonschema.Schema{ ID: schema.ToPtr("test"), }) }) @@ -87,7 +87,7 @@ func TestConfigurationServer(t *testing.T) { t.Errorf("POST /validate: expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /validate", res, http.StatusOK, schema.ValidateResponse{ + assertHTTPResponse(t, res, http.StatusOK, schema.ValidateResponse{ Schema: mockSchema, Capabilities: mockCapabilities, ResolvedConfiguration: `{"version":1}`, @@ -100,7 +100,7 @@ func TestConfigurationServer(t *testing.T) { t.Errorf("POST /validate: expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /validate", res, http.StatusBadRequest, schema.ErrorResponse{ + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ Message: "failed to decode json request body", Details: map[string]string{ "cause": "json: cannot unmarshal string into Go value of type connector.mockRawConfiguration", diff --git a/connector/server_test.go b/connector/server_test.go index 934a643..edf60b0 100644 --- a/connector/server_test.go +++ b/connector/server_test.go @@ -222,27 +222,27 @@ func assertHTTPResponseStatus(t *testing.T, name string, res *http.Response, sta } } -func assertHTTPResponse[B any](t *testing.T, name string, res *http.Response, statusCode int, expectedBody B) { +func assertHTTPResponse[B any](t *testing.T, res *http.Response, statusCode int, expectedBody B) { bodyBytes, err := io.ReadAll(res.Body) if err != nil { - t.Errorf("%s: failed to read response body", name) + t.Error("failed to read response body") t.FailNow() } if res.StatusCode != statusCode { - t.Errorf("%s: expected status %d, got %d. Body: %s", name, statusCode, res.StatusCode, string(bodyBytes)) + t.Errorf("expected status %d, got %d. Body: %s", statusCode, res.StatusCode, string(bodyBytes)) t.FailNow() } var body B if err = json.Unmarshal(bodyBytes, &body); err != nil { - t.Errorf("%s: failed to decode json body, got error: %s; body: %s", name, err, string(bodyBytes)) + t.Errorf("failed to decode json body, got error: %s; body: %s", err, string(bodyBytes)) t.FailNow() } if !internal.DeepEqual(body, expectedBody) { expectedBytes, _ := json.Marshal(expectedBody) - t.Errorf("%s.\nexpect: %+v\ngot: %+v", name, string(expectedBytes), string(bodyBytes)) + t.Errorf("expect: %+v\ngot: %+v", string(expectedBytes), string(bodyBytes)) t.FailNow() } } @@ -368,32 +368,36 @@ func TestServerAuth(t *testing.T) { httpServer := buildTestServer(server) defer httpServer.Close() - res, err := http.Get(fmt.Sprintf("%s/schema", httpServer.URL)) - if err != nil { - t.Errorf("Unauthorized GET /schema: expected no error, got %s", err) - t.FailNow() - } - assertHTTPResponse(t, "Unauthorized GET /schema", res, http.StatusUnauthorized, schema.ErrorResponse{ - Message: "Unauthorized", - Details: map[string]any{ - "cause": "Bearer token does not match.", - }, + t.Run("Unauthorized GET /schema", func(t *testing.T) { + res, err := http.Get(fmt.Sprintf("%s/schema", httpServer.URL)) + if err != nil { + t.Errorf("expected no error, got %s", err) + t.FailNow() + } + assertHTTPResponse(t, res, http.StatusUnauthorized, schema.ErrorResponse{ + Message: "Unauthorized", + Details: map[string]any{ + "cause": "Bearer token does not match.", + }, + }) }) - authRequest, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/schema", httpServer.URL), nil) - if err != nil { - t.Errorf("Authorized GET /schema: expected no error, got %s", err) - t.FailNow() - } - authRequest.Header.Add("Authorization", "Bearer random-secret") + t.Run("Authorized GET /schema", func(t *testing.T) { + authRequest, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/schema", httpServer.URL), nil) + if err != nil { + t.Errorf("expected no error, got %s", err) + t.FailNow() + } + authRequest.Header.Add("Authorization", "Bearer random-secret") - res, err = http.DefaultClient.Do(authRequest) - if err != nil { - t.Errorf("Authorized GET /schema: expected no error, got %s", err) - t.FailNow() - } + res, err := http.DefaultClient.Do(authRequest) + if err != nil { + t.Errorf("expected no error, got %s", err) + t.FailNow() + } - assertHTTPResponse(t, "Authorized GET /schema", res, http.StatusOK, mockSchema) + assertHTTPResponse(t, res, http.StatusOK, mockSchema) + }) } func TestServerConnector(t *testing.T) { @@ -413,16 +417,16 @@ func TestServerConnector(t *testing.T) { t.Run("GET /capabilities", func(t *testing.T) { res, err := http.Get(fmt.Sprintf("%s/capabilities", httpServer.URL)) if err != nil { - t.Errorf("GET /capabilities: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "GET /capabilities", res, http.StatusOK, mockCapabilities) + assertHTTPResponse(t, res, http.StatusOK, mockCapabilities) }) t.Run("GET /health", func(t *testing.T) { res, err := http.Get(fmt.Sprintf("%s/health", httpServer.URL)) if err != nil { - t.Errorf("GET /health: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } assertHTTPResponseStatus(t, "GET /health", res, http.StatusNoContent) @@ -431,7 +435,7 @@ func TestServerConnector(t *testing.T) { t.Run("GET /metrics", func(t *testing.T) { res, err := http.Get(fmt.Sprintf("%s/metrics", httpServer.URL)) if err != nil { - t.Errorf("GET /metrics: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } assertHTTPResponseStatus(t, "GET /metrics", res, http.StatusOK) @@ -446,10 +450,10 @@ func TestServerConnector(t *testing.T) { Variables: []schema.QueryRequestVariablesElem{}, }) if err != nil { - t.Errorf("POST /query: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /query", res, http.StatusOK, schema.QueryResponse{ + assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{ { Aggregates: schema.RowSetAggregates{}, Rows: []schema.Row{ @@ -466,10 +470,10 @@ func TestServerConnector(t *testing.T) { t.Run("POST /query - json decode failure", func(t *testing.T) { res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), "") if err != nil { - t.Errorf("POST /query: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /query", res, http.StatusBadRequest, schema.ErrorResponse{ + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ Message: "failed to decode json request body", Details: map[string]any{ "cause": "json: cannot unmarshal string into Go value of type map[string]interface {}", @@ -486,10 +490,10 @@ func TestServerConnector(t *testing.T) { Variables: []schema.QueryRequestVariablesElem{}, }) if err != nil { - t.Errorf("POST /query: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /query", res, http.StatusBadRequest, schema.ErrorResponse{ + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ Message: "collection not found: test", Details: map[string]any{}, }) @@ -506,10 +510,10 @@ func TestServerConnector(t *testing.T) { CollectionRelationships: schema.MutationRequestCollectionRelationships{}, }) if err != nil { - t.Errorf("POST /mutation: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /mutation", res, http.StatusOK, schema.MutationResponse{ + assertHTTPResponse(t, res, http.StatusOK, schema.MutationResponse{ OperationResults: []schema.MutationOperationResults{ { AffectedRows: 1, @@ -521,10 +525,10 @@ func TestServerConnector(t *testing.T) { t.Run("POST /mutation - json decode failure", func(t *testing.T) { res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), "") if err != nil { - t.Errorf("POST /mutation: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /mutation", res, http.StatusBadRequest, schema.ErrorResponse{ + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ Message: "failed to decode json request body", Details: map[string]any{ "cause": "json: cannot unmarshal string into Go value of type map[string]interface {}", @@ -543,17 +547,17 @@ func TestServerConnector(t *testing.T) { CollectionRelationships: schema.MutationRequestCollectionRelationships{}, }) if err != nil { - t.Errorf("POST /mutation: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /mutation", res, http.StatusBadRequest, schema.ErrorResponse{ + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ Message: "operation not found: test", Details: map[string]any{}, }) }) - t.Run("POST /explain", func(t *testing.T) { - res, err := httpPostJSON(fmt.Sprintf("%s/explain", httpServer.URL), schema.QueryRequest{ + t.Run("POST /query/explain", func(t *testing.T) { + res, err := httpPostJSON(fmt.Sprintf("%s/query/explain", httpServer.URL), schema.QueryRequest{ Collection: "articles", Arguments: schema.QueryRequestArguments{}, CollectionRelationships: schema.QueryRequestCollectionRelationships{}, @@ -561,25 +565,53 @@ func TestServerConnector(t *testing.T) { Variables: []schema.QueryRequestVariablesElem{}, }) if err != nil { - t.Errorf("POST /explain: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /explain", res, http.StatusOK, schema.ExplainResponse{ + assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ Details: schema.ExplainResponseDetails{}, }) }) - t.Run("POST /explain - json decode failure", func(t *testing.T) { - res, err := httpPostJSON(fmt.Sprintf("%s/explain", httpServer.URL), schema.QueryRequest{}) + t.Run("POST /query/explain - json decode failure", func(t *testing.T) { + res, err := httpPostJSON(fmt.Sprintf("%s/query/explain", httpServer.URL), schema.QueryRequest{}) if err != nil { - t.Errorf("POST /mutation: expected no error, got %s", err) + t.Errorf("expected no error, got %s", err) t.FailNow() } - assertHTTPResponse(t, "POST /explain", res, http.StatusBadRequest, schema.ErrorResponse{ + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ Message: "failed to decode json request body", Details: map[string]any{ "cause": "field arguments in QueryRequest: required", }, }) }) + + t.Run("POST /mutation/explain", func(t *testing.T) { + res, err := httpPostJSON(fmt.Sprintf("%s/mutation/explain", httpServer.URL), schema.MutationRequest{ + Operations: []schema.MutationOperation{}, + CollectionRelationships: make(schema.MutationRequestCollectionRelationships), + }) + if err != nil { + t.Errorf("expected no error, got %s", err) + t.FailNow() + } + assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ + Details: schema.ExplainResponseDetails{}, + }) + }) + + t.Run("POST /mutation/explain - json decode failure", func(t *testing.T) { + res, err := httpPostJSON(fmt.Sprintf("%s/mutation/explain", httpServer.URL), schema.MutationRequest{}) + if err != nil { + t.Errorf("expected no error, got %s", err) + t.FailNow() + } + assertHTTPResponse(t, res, http.StatusBadRequest, schema.ErrorResponse{ + Message: "failed to decode json request body", + Details: map[string]any{ + "cause": "field collection_relationships in MutationRequest: required", + }, + }) + }) } diff --git a/example/codegen/connector.generated.go b/example/codegen/connector.generated.go deleted file mode 100644 index bd0ef39..0000000 --- a/example/codegen/connector.generated.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - "github.com/go-viper/mapstructure/v2" - "github.com/hasura/ndc-sdk-go/schema" -) - -func (c *Connector) GetSchema(configuration *Configuration) (*schema.SchemaResponse, error) { - return &schema.SchemaResponse{}, nil -} - -func (c *Connector) Query(ctx context.Context, configuration *Configuration, state *State, request *schema.QueryRequest) (schema.QueryResponse, error) { - requestVars := request.Variables - if len(requestVars) == 0 { - requestVars = []schema.QueryRequestVariablesElem{{}} - } - - var rowSets []schema.RowSet - - for _, requestVar := range requestVars { - rawResult, err := execQuery(ctx, configuration, state, request, requestVar) - if err != nil { - return nil, err - } - result, err := PruneFields(request.Query.Fields, rawResult) - if err != nil { - return nil, err - } - rowSets = append(rowSets, schema.RowSet{ - Aggregates: schema.RowSetAggregates{}, - Rows: []schema.Row{ - map[string]any{ - "__value": result, - }, - }, - }) - } - - return rowSets, nil -} - -func (c *Connector) Mutation(ctx context.Context, configuration *Configuration, state *State, request *schema.MutationRequest) (*schema.MutationResponse, error) { - operationResults := make([]schema.MutationOperationResults, 0, len(request.Operations)) - - for _, operation := range request.Operations { - rawResult, err := execProcedure(ctx, configuration, state, request, &operation) - if err != nil { - return nil, err - } - result, err := PruneFields(operation.Fields, rawResult) - if err != nil { - return nil, err - } - operationResults = append(operationResults, schema.MutationOperationResults{ - AffectedRows: 1, - Returning: []schema.Row{ - map[string]any{ - "__value": result, - }, - }, - }) - } - - return &schema.MutationResponse{ - OperationResults: operationResults, - }, nil -} - -func execQuery(ctx context.Context, configuration *Configuration, state *State, request *schema.QueryRequest, variables map[string]any) (any, error) { - - switch request.Collection { - case "hello": - args, err := ResolveArguments[HelloArguments](request.Arguments, variables) - if err != nil { - return nil, err - } - return Hello(ctx, state, args) - default: - return nil, fmt.Errorf("unsupported query: %s", request.Collection) - } -} - -func execProcedure(ctx context.Context, configuration *Configuration, state *State, request *schema.MutationRequest, operation *schema.MutationOperation) (any, error) { - switch operation.Name { - case "create_author": - var args CreateAuthorArguments - if err := json.Unmarshal(operation.Arguments, &args); err != nil { - return nil, schema.BadRequestError("failed to decode arguments", map[string]any{ - "cause": err.Error(), - }) - } - return CreateAuthor(ctx, state, &args) - default: - return nil, fmt.Errorf("unsupported procedure operation: %s", operation.Name) - } -} - -// PruneFields prune unnecessary fields from selection -func PruneFields(fields map[string]schema.Field, result any) (any, error) { - if len(fields) == 0 { - return result, nil - } - - if result == nil { - return nil, errors.New("expected object fields, got nil") - } - - var outputMap map[string]any - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - Result: &outputMap, - TagName: "json", - }) - if err != nil { - return nil, err - } - if err := decoder.Decode(result); err != nil { - return nil, err - } - - output := make(map[string]any) - for key, field := range fields { - f, err := field.Interface() - switch fi := f.(type) { - case schema.ColumnField: - if col, ok := outputMap[fi.Column]; ok { - output[fi.Column] = col - } else { - output[fi.Column] = nil - } - case schema.RelationshipField: - return nil, fmt.Errorf("unsupported relationship field, %s", key) - default: - return nil, err - } - } - - return output, nil -} - -// ResolveArguments resolve variables in arguments and map them to struct -func ResolveArguments[R any](arguments map[string]schema.Argument, variables map[string]any) (*R, error) { - resolvedArgs, err := ResolveArgumentVariables(arguments, variables) - if err != nil { - return nil, err - } - - var result R - - if err = mapstructure.Decode(resolvedArgs, &result); err != nil { - return nil, err - } - - return &result, nil -} - -// ResolveArgumentVariables resolve variables in arguments if exist -func ResolveArgumentVariables(arguments map[string]schema.Argument, variables map[string]any) (map[string]any, error) { - results := make(map[string]any) - for key, arg := range arguments { - switch arg.Type { - case schema.ArgumentTypeLiteral: - results[key] = arg.Value - case schema.ArgumentTypeVariable: - value, ok := variables[arg.Name] - if !ok { - return nil, fmt.Errorf("variable %s not found", arg.Name) - } - results[key] = value - default: - return nil, fmt.Errorf("unsupported argument type: %s", arg.Type) - } - } - - return results, nil -} diff --git a/example/codegen/connector.go b/example/codegen/connector.go deleted file mode 100644 index 0a1f576..0000000 --- a/example/codegen/connector.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "context" - _ "embed" - - "github.com/hasura/ndc-sdk-go/connector" - "github.com/hasura/ndc-sdk-go/schema" - "github.com/swaggest/jsonschema-go" -) - -type RawConfiguration struct{} -type Configuration struct{} - -type State struct{} -type Connector struct{} - -func (connector *Connector) GetRawConfigurationSchema() *jsonschema.Schema { - return nil -} -func (connector *Connector) MakeEmptyConfiguration() *RawConfiguration { - return &RawConfiguration{} -} - -func (connector *Connector) UpdateConfiguration(ctx context.Context, rawConfiguration *RawConfiguration) (*RawConfiguration, error) { - return &RawConfiguration{}, nil -} -func (connector *Connector) ValidateRawConfiguration(rawConfiguration *RawConfiguration) (*Configuration, error) { - return &Configuration{}, nil -} - -func (connector *Connector) TryInitState(configuration *Configuration, metrics *connector.TelemetryState) (*State, error) { - - return &State{}, nil -} - -func (mc *Connector) HealthCheck(ctx context.Context, configuration *Configuration, state *State) error { - return nil -} - -func (mc *Connector) GetCapabilities(configuration *Configuration) *schema.CapabilitiesResponse { - return &schema.CapabilitiesResponse{ - Version: "^0.1.0", - Capabilities: schema.Capabilities{ - Query: schema.QueryCapabilities{ - Variables: schema.LeafCapability{}, - }, - }, - } -} - -func (mc *Connector) QueryExplain(ctx context.Context, configuration *Configuration, state *State, request *schema.QueryRequest) (*schema.ExplainResponse, error) { - return nil, schema.NotSupportedError("query explain has not been supported yet", nil) -} - -func (mc *Connector) MutationExplain(ctx context.Context, configuration *Configuration, state *State, request *schema.MutationRequest) (*schema.ExplainResponse, error) { - return nil, schema.NotSupportedError("mutation explain has not been supported yet", nil) -} diff --git a/example/codegen/main.go b/example/codegen/main.go deleted file mode 100644 index 443be6a..0000000 --- a/example/codegen/main.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import "github.com/hasura/ndc-sdk-go/connector" - -func main() { - if err := connector.Start[RawConfiguration, Configuration, State]( - &Connector{}, - connector.WithMetricsPrefix("ndc_codegen"), - connector.WithDefaultServiceName("ndc_codegen"), - connector.WithoutConfig(), - ); err != nil { - panic(err) - } -} diff --git a/example/codegen/query.go b/example/codegen/query.go deleted file mode 100644 index c58145d..0000000 --- a/example/codegen/query.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "context" -) - -// A hello argument -type HelloArguments struct { - Num int `json:"num"` - Str string `json:"str"` -} - -// A hello result -type HelloResult struct { - Num int `json:"num"` - Str string `json:"str"` -} - -// Send hello message -// -// @function hello -func Hello(ctx context.Context, state *State, arguments *HelloArguments) (*HelloResult, error) { - return &HelloResult{ - Num: 1, - Str: "world", - }, nil -} - -// A create author argument -type CreateAuthorArguments struct { - Name string `json:"name"` -} - -// A create author result -type CreateAuthorResult struct { - ID int `json:"id"` - Name string `json:"name"` -} - -// A procedure example -// -// @procedure -func CreateAuthor(ctx context.Context, state *State, arguments *CreateAuthorArguments) (*CreateAuthorResult, error) { - return &CreateAuthorResult{ - ID: 1, - Name: arguments.Name, - }, nil -} diff --git a/go.mod b/go.mod index 651d69a..145eb50 100644 --- a/go.mod +++ b/go.mod @@ -4,21 +4,20 @@ go 1.18 require ( github.com/alecthomas/kong v0.8.1 - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 github.com/prometheus/client_golang v1.18.0 - github.com/rs/zerolog v1.31.0 + github.com/rs/zerolog v1.32.0 github.com/swaggest/jsonschema-go v0.3.64 - go.opentelemetry.io/otel v1.22.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.45.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 - go.opentelemetry.io/otel/exporters/prometheus v0.45.0 - go.opentelemetry.io/otel/metric v1.22.0 - go.opentelemetry.io/otel/sdk v1.22.0 - go.opentelemetry.io/otel/sdk/metric v1.22.0 - go.opentelemetry.io/otel/trace v1.22.0 + go.opentelemetry.io/otel v1.23.1 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 + go.opentelemetry.io/otel/exporters/prometheus v0.45.2 + go.opentelemetry.io/otel/metric v1.23.1 + go.opentelemetry.io/otel/sdk v1.23.1 + go.opentelemetry.io/otel/sdk/metric v1.23.1 + go.opentelemetry.io/otel/trace v1.23.1 ) require ( @@ -28,7 +27,7 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -36,11 +35,11 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/swaggest/refl v1.3.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect - google.golang.org/grpc v1.60.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect + google.golang.org/grpc v1.61.0 // indirect google.golang.org/protobuf v1.32.0 // indirect ) diff --git a/go.sum b/go.sum index 944d947..9ab3a6b 100644 --- a/go.sum +++ b/go.sum @@ -17,16 +17,14 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -46,8 +44,8 @@ github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1B github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= @@ -57,48 +55,48 @@ github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= -go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.45.0 h1:tfil6di0PoNV7FZdsCS7A5izZoVVQ7AuXtyekbOpG/I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.45.0/go.mod h1:AKFZIEPOnqB00P63bTjOiah4ZTaRzl1TKwUWpZdYUHI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0 h1:+RbSCde0ERway5FwKvXR3aRJIFeDu9rtwC6E7BC6uoM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0/go.mod h1:zcI8u2EJxbLPyoZ3SkVAAcQPgYb1TDRzW93xLFnsggU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= -go.opentelemetry.io/otel/exporters/prometheus v0.45.0 h1:BeIK2KGho0oCWa7LxEGSqfDZbs7Fpv/Viz+FS4P8CXE= -go.opentelemetry.io/otel/exporters/prometheus v0.45.0/go.mod h1:UVJZPLnfDSvHj+eJuZE+E1GjIBD267mEMfAAHJdghWg= -go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= -go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/sdk/metric v1.22.0 h1:ARrRetm1HCVxq0cbnaZQlfwODYJHo3gFL8Z3tSmHBcI= -go.opentelemetry.io/otel/sdk/metric v1.22.0/go.mod h1:KjQGeMIDlBNEOo6HvjhxIec1p/69/kULDcp4gr0oLQQ= -go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= -go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY= +go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 h1:ZqRWZJGHXV/1yCcEEVJ6/Uz2JtM79DNS8OZYa3vVY/A= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1/go.mod h1:D7ynngPWlGJrqyGSDOdscuv7uqttfCE3jcBvffDv9y4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1 h1:q/Nj5/2TZRIt6PderQ9oU0M00fzoe8UZuINGw6ETGTw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1/go.mod h1:DTE9yAu6r08jU3xa68GiSeI7oRcSEQ2RpKbbQGO+dWM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1 h1:p3A5+f5l9e/kuEBwLOrnpkIDHQFlHmbiVxMURWRK6gQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1/go.mod h1:OClrnXUjBqQbInvjJFjYSnMxBSCXBF8r3b34WqjiIrQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 h1:cfuy3bXmLJS7M1RZmAL6SuhGtKUp2KEsrm00OlAXkq4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1/go.mod h1:22jr92C6KwlwItJmQzfixzQM3oyyuYLCfHiMY+rpsPU= +go.opentelemetry.io/otel/exporters/prometheus v0.45.2 h1:pe2Jqk1K18As0RCw7J08QhgXNqr+6npx0a5W4IgAFA8= +go.opentelemetry.io/otel/exporters/prometheus v0.45.2/go.mod h1:B38pscHKI6bhFS44FDw0eFU3iqG3ASNIvY+fZgR5sAc= +go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo= +go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI= +go.opentelemetry.io/otel/sdk v1.23.1 h1:O7JmZw0h76if63LQdsBMKQDWNb5oEcOThG9IrxscV+E= +go.opentelemetry.io/otel/sdk v1.23.1/go.mod h1:LzdEVR5am1uKOOwfBWFef2DCi1nu3SA8XQxx2IerWFk= +go.opentelemetry.io/otel/sdk/metric v1.23.1 h1:T9/8WsYg+ZqIpMWwdISVVrlGb/N0Jr1OHjR/alpKwzg= +go.opentelemetry.io/otel/sdk/metric v1.23.1/go.mod h1:8WX6WnNtHCgUruJ4TJ+UssQjMtpxkpX0zveQC8JG/E0= +go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8= +go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI= go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 h1:nz5NESFLZbJGPFxDT/HCn+V1mZ8JGNoY4nUpmW/Y2eg= -google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s= -google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo= +google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A= +google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 h1:FSL3lRCkhaPFxqi0s9o+V4UI2WTzAVOvkgbd4kVV4Wg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= diff --git a/schema/extend.go b/schema/extend.go index c6def4e..5b9d19c 100644 --- a/schema/extend.go +++ b/schema/extend.go @@ -603,14 +603,14 @@ func (j *Field) UnmarshalJSON(b []byte) error { results["column"] = column // decode fields + var fields NestedField rawFields, ok := raw["fields"] if ok { - var fields NestedField if err = json.Unmarshal(rawFields, &fields); err != nil { return fmt.Errorf("field fields in Field: %s", err) } - results["fields"] = fields } + results["fields"] = fields case FieldTypeRelationship: relationship, err := unmarshalStringFromJsonMap(raw, "relationship", true) if err != nil { diff --git a/schema/schema_test.go b/schema/schema_test.go index 567f835..bd52978 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -2,234 +2,51 @@ package schema import ( "encoding/json" + "net/http" "testing" "github.com/hasura/ndc-sdk-go/internal" ) func TestSchemaResponse(t *testing.T) { - rawInput := []byte(`{ - "scalar_types": { - "Int": { - "aggregate_functions": { - "max": { - "result_type": { - "type": "nullable", - "underlying_type": { - "type": "named", - "name": "Int" - } - } - }, - "min": { - "result_type": { - "type": "nullable", - "underlying_type": { - "type": "named", - "name": "Int" - } - } - } - }, - "comparison_operators": {} - }, - "String": { - "aggregate_functions": {}, - "comparison_operators": { - "like": { - "argument_type": { - "type": "named", - "name": "String" - } - } - } - } - }, - "object_types": { - "article": { - "description": "An article", - "fields": { - "author_id": { - "description": "The article's author ID", - "type": { - "type": "named", - "name": "Int" - } - }, - "id": { - "description": "The article's primary key", - "type": { - "type": "named", - "name": "Int" - } - }, - "title": { - "description": "The article's title", - "type": { - "type": "named", - "name": "String" - } - } - } - }, - "author": { - "description": "An author", - "fields": { - "first_name": { - "description": "The author's first name", - "type": { - "type": "named", - "name": "String" - } - }, - "id": { - "description": "The author's primary key", - "type": { - "type": "named", - "name": "Int" - } - }, - "last_name": { - "description": "The author's last name", - "type": { - "type": "named", - "name": "String" - } - } - } - } - }, - "collections": [ - { - "name": "articles", - "description": "A collection of articles", - "arguments": {}, - "type": "article", - "uniqueness_constraints": { - "ArticleByID": { - "unique_columns": [ - "id" - ] - } - }, - "foreign_keys": { - "Article_AuthorID": { - "column_mapping": { - "author_id": "id" - }, - "foreign_collection": "authors" - } - } - }, - { - "name": "authors", - "description": "A collection of authors", - "arguments": {}, - "type": "author", - "uniqueness_constraints": { - "AuthorByID": { - "unique_columns": [ - "id" - ] - } - }, - "foreign_keys": {} - }, - { - "name": "articles_by_author", - "description": "Articles parameterized by author", - "arguments": { - "author_id": { - "type": { - "type": "named", - "name": "Int" - } - } - }, - "type": "article", - "uniqueness_constraints": {}, - "foreign_keys": {} - } - ], - "functions": [ - { - "name": "latest_article_id", - "description": "Get the ID of the most recent article", - "arguments": {}, - "result_type": { - "type": "nullable", - "underlying_type": { - "type": "named", - "name": "Int" - } - } - } - ], - "procedures": [ - { - "name": "upsert_article", - "description": "Insert or update an article", - "arguments": { - "article": { - "description": "The article to insert or update", - "type": { - "type": "named", - "name": "article" - } - } - }, - "result_type": { - "type": "nullable", - "underlying_type": { - "type": "named", - "name": "article" - } - } - } - ] - }`) - - var resp SchemaResponse - if err := json.Unmarshal(rawInput, &resp); err != nil { - t.Errorf("failed to decode SchemaResponse: %s", err) - t.FailNow() - } - - if intScalar, ok := resp.ScalarTypes["Int"]; !ok { - t.Error("Int scalar in SchemaResponse: required") - t.FailNow() - } else if len(intScalar.ComparisonOperators) != 0 { - t.Errorf("Int scalar in SchemaResponse: expected no comparison operator, got: %+v", intScalar.ComparisonOperators) - t.FailNow() - } else if !internal.DeepEqual(intScalar.AggregateFunctions, ScalarTypeAggregateFunctions{ - "max": AggregateFunctionDefinition{ - ResultType: NewNullableNamedType("Int").Encode(), - }, - "min": AggregateFunctionDefinition{ - ResultType: NewNullableNamedType("Int").Encode(), - }, - }) { - t.Errorf("Int scalar in SchemaResponse: expected equal aggregate functions; %+v", intScalar.AggregateFunctions) - t.FailNow() - } else if _, err := intScalar.AggregateFunctions["max"].ResultType.AsNullable(); err != nil { - t.Errorf("Int scalar in SchemaResponse: expected aggregate function max, got error: %s", err) + // reuse test fixtures from ndc-reference + httpResp, err := http.Get("https://raw.githubusercontent.com/hasura/ndc-spec/main/ndc-reference/tests/schema/expected.json") + if err != nil { + t.Errorf("failed to fetch schema: %s", err.Error()) t.FailNow() } - if stringScalar, ok := resp.ScalarTypes["String"]; !ok { - t.Error("String scalar in SchemaResponse: required") - t.FailNow() - } else if len(stringScalar.AggregateFunctions) != 0 { - t.Errorf("Int scalar in SchemaResponse: expected no aggregate function, got: %+v", stringScalar.AggregateFunctions) - t.FailNow() - } else if !internal.DeepEqual(stringScalar.ComparisonOperators, map[string]ComparisonOperatorDefinition{ - "like": NewComparisonOperatorCustom(NewNamedType("String")).Encode(), - }) { - t.Errorf("String scalar in SchemaResponse: expected equal comparison operators; %+v", stringScalar.ComparisonOperators) + var resp SchemaResponse + if err = json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { + t.Errorf("failed to decode schema json: %s", err.Error()) t.FailNow() } expected := SchemaResponse{ + ScalarTypes: SchemaResponseScalarTypes{ + "Int": ScalarType{ + AggregateFunctions: ScalarTypeAggregateFunctions{ + "max": AggregateFunctionDefinition{ + ResultType: NewNullableNamedType("Int").Encode(), + }, + "min": AggregateFunctionDefinition{ + ResultType: NewNullableNamedType("Int").Encode(), + }, + }, + ComparisonOperators: map[string]ComparisonOperatorDefinition{ + "eq": NewComparisonOperatorEqual().Encode(), + "in": NewComparisonOperatorIn().Encode(), + }, + }, + "String": { + AggregateFunctions: ScalarTypeAggregateFunctions{}, + ComparisonOperators: map[string]ComparisonOperatorDefinition{ + "eq": NewComparisonOperatorEqual().Encode(), + "in": NewComparisonOperatorIn().Encode(), + "like": NewComparisonOperatorCustom(NewNamedType("String")).Encode(), + }, + }, + }, ObjectTypes: SchemaResponseObjectTypes{ "article": ObjectType{ Description: ToPtr("An article"), @@ -238,11 +55,11 @@ func TestSchemaResponse(t *testing.T) { Description: ToPtr("The article's author ID"), Type: NewNamedType("Int").Encode(), }, - "id": ObjectField{ + "id": { Description: ToPtr("The article's primary key"), Type: NewNamedType("Int").Encode(), }, - "title": ObjectField{ + "title": { Description: ToPtr("The article's title"), Type: NewNamedType("String").Encode(), }, @@ -251,20 +68,79 @@ func TestSchemaResponse(t *testing.T) { "author": ObjectType{ Description: ToPtr("An author"), Fields: ObjectTypeFields{ - "first_name": ObjectField{ + "first_name": { Description: ToPtr("The author's first name"), Type: NewNamedType("String").Encode(), }, - "id": ObjectField{ + "id": { Description: ToPtr("The author's primary key"), Type: NewNamedType("Int").Encode(), }, - "last_name": ObjectField{ + "last_name": { Description: ToPtr("The author's last name"), Type: NewNamedType("String").Encode(), }, }, }, + "institution": ObjectType{ + Description: ToPtr("An institution"), + Fields: ObjectTypeFields{ + "departments": ObjectField{ + Description: ToPtr("The institution's departments"), + Type: NewArrayType(NewNamedType("String")).Encode(), + }, + "id": ObjectField{ + Description: ToPtr("The institution's primary key"), + Type: NewNamedType("Int").Encode(), + }, + "location": ObjectField{ + Description: ToPtr("The institution's location"), + Type: NewNamedType("location").Encode(), + }, + "name": ObjectField{ + Description: ToPtr("The institution's name"), + Type: NewNamedType("String").Encode(), + }, + "staff": ObjectField{ + Description: ToPtr("The institution's staff"), + Type: NewArrayType(NewNamedType("staff_member")).Encode(), + }, + }, + }, + "location": ObjectType{ + Description: ToPtr("A location"), + Fields: ObjectTypeFields{ + "campuses": ObjectField{ + Description: ToPtr("The location's campuses"), + Type: NewArrayType(NewNamedType("String")).Encode(), + }, + "city": ObjectField{ + Description: ToPtr("The location's city"), + Type: NewNamedType("String").Encode(), + }, + "country": ObjectField{ + Description: ToPtr("The location's country"), + Type: NewNamedType("String").Encode(), + }, + }, + }, + "staff_member": ObjectType{ + Description: ToPtr("A staff member"), + Fields: ObjectTypeFields{ + "first_name": ObjectField{ + Description: ToPtr("The staff member's first name"), + Type: NewNamedType("String").Encode(), + }, + "last_name": ObjectField{ + Description: ToPtr("The staff member's last name"), + Type: NewNamedType("String").Encode(), + }, + "specialities": ObjectField{ + Description: ToPtr("The staff member's specialities"), + Type: NewArrayType(NewNamedType("String")).Encode(), + }, + }, + }, }, Collections: []CollectionInfo{ { @@ -298,6 +174,18 @@ func TestSchemaResponse(t *testing.T) { }, ForeignKeys: CollectionInfoForeignKeys{}, }, + { + Name: "institutions", + Description: ToPtr("A collection of institutions"), + Arguments: CollectionInfoArguments{}, + Type: "institution", + UniquenessConstraints: CollectionInfoUniquenessConstraints{ + "InstitutionByID": UniquenessConstraint{ + UniqueColumns: []string{"id"}, + }, + }, + ForeignKeys: CollectionInfoForeignKeys{}, + }, { Name: "articles_by_author", Description: ToPtr("Articles parameterized by author"), @@ -331,12 +219,19 @@ func TestSchemaResponse(t *testing.T) { }, ResultType: NewNullableNamedType("article").Encode(), }, + { + Name: "delete_articles", + Description: ToPtr("Delete articles which match a predicate"), + Arguments: ProcedureInfoArguments{ + "where": ArgumentInfo{ + Description: ToPtr("The predicate"), + Type: NewPredicateType("article").Encode(), + }, + }, + ResultType: NewArrayType(NewNamedType("article")).Encode(), + }, }, } - if !internal.DeepEqual(expected.ObjectTypes, resp.ObjectTypes) { - t.Errorf("object_types in SchemaResponse: unexpected equality;\nexpected: %+v,\n got: %+v\n", expected.ObjectTypes, resp.ObjectTypes) - t.FailNow() - } if !internal.DeepEqual(expected.Collections, resp.Collections) { t.Errorf("collections in SchemaResponse: unexpected equality;\nexpected: %+v,\n got: %+v\n", expected.Collections, resp.Collections)