From 229d467c0aaac448f297d4e0f4f2527167be7e14 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Tue, 1 Oct 2024 11:15:03 +0700 Subject: [PATCH] Implement custom json.Marshaler for RowSet (#153) --- example/reference/connector.go | 473 +++++++++++++++------------- example/reference/connector_test.go | 145 ++++----- example/reference/go.mod.build | 2 +- schema/query.go | 13 + 4 files changed, 321 insertions(+), 312 deletions(-) diff --git a/example/reference/connector.go b/example/reference/connector.go index 78a84f9..ffada19 100644 --- a/example/reference/connector.go +++ b/example/reference/connector.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "fmt" + "math" "reflect" "regexp" + "slices" "sort" "strings" @@ -70,6 +72,225 @@ func (s *State) GetLatestArticle() *Article { return &latestArticle } +var ndcSchema = schema.SchemaResponse{ + ScalarTypes: schema.SchemaResponseScalarTypes{ + "Int": schema.ScalarType{ + AggregateFunctions: schema.ScalarTypeAggregateFunctions{ + "max": schema.AggregateFunctionDefinition{ + ResultType: schema.NewNullableNamedType("Int").Encode(), + }, + "min": schema.AggregateFunctionDefinition{ + ResultType: schema.NewNullableNamedType("Int").Encode(), + }, + }, + ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{ + "eq": schema.NewComparisonOperatorEqual().Encode(), + "in": schema.NewComparisonOperatorIn().Encode(), + }, + Representation: schema.NewTypeRepresentationInteger().Encode(), + }, + "String": { + AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, + ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{ + "eq": schema.NewComparisonOperatorEqual().Encode(), + "in": schema.NewComparisonOperatorIn().Encode(), + "like": schema.NewComparisonOperatorCustom(schema.NewNamedType("String")).Encode(), + }, + Representation: schema.NewTypeRepresentationString().Encode(), + }, + }, + ObjectTypes: schema.SchemaResponseObjectTypes{ + "article": schema.ObjectType{ + Description: utils.ToPtr("An article"), + Fields: schema.ObjectTypeFields{ + "author_id": schema.ObjectField{ + Description: utils.ToPtr("The article's author ID"), + Type: schema.NewNamedType("Int").Encode(), + }, + "id": { + Description: utils.ToPtr("The article's primary key"), + Type: schema.NewNamedType("Int").Encode(), + }, + "title": { + Description: utils.ToPtr("The article's title"), + Type: schema.NewNamedType("String").Encode(), + }, + }, + }, + "author": schema.ObjectType{ + Description: utils.ToPtr("An author"), + Fields: schema.ObjectTypeFields{ + "first_name": { + Description: utils.ToPtr("The author's first name"), + Type: schema.NewNamedType("String").Encode(), + }, + "id": { + Description: utils.ToPtr("The author's primary key"), + Type: schema.NewNamedType("Int").Encode(), + }, + "last_name": { + Description: utils.ToPtr("The author's last name"), + Type: schema.NewNamedType("String").Encode(), + }, + }, + }, + "institution": schema.ObjectType{ + Description: utils.ToPtr("An institution"), + Fields: schema.ObjectTypeFields{ + "departments": schema.ObjectField{ + Description: utils.ToPtr("The institution's departments"), + Type: schema.NewArrayType(schema.NewNamedType("String")).Encode(), + }, + "id": schema.ObjectField{ + Description: utils.ToPtr("The institution's primary key"), + Type: schema.NewNamedType("Int").Encode(), + }, + "location": schema.ObjectField{ + Description: utils.ToPtr("The institution's location"), + Type: schema.NewNamedType("location").Encode(), + }, + "name": schema.ObjectField{ + Description: utils.ToPtr("The institution's name"), + Type: schema.NewNamedType("String").Encode(), + }, + "staff": schema.ObjectField{ + Description: utils.ToPtr("The institution's staff"), + Type: schema.NewArrayType(schema.NewNamedType("staff_member")).Encode(), + }, + }, + }, + "location": schema.ObjectType{ + Description: utils.ToPtr("A location"), + Fields: schema.ObjectTypeFields{ + "campuses": schema.ObjectField{ + Description: utils.ToPtr("The location's campuses"), + Type: schema.NewArrayType(schema.NewNamedType("String")).Encode(), + }, + "city": schema.ObjectField{ + Description: utils.ToPtr("The location's city"), + Type: schema.NewNamedType("String").Encode(), + }, + "country": schema.ObjectField{ + Description: utils.ToPtr("The location's country"), + Type: schema.NewNamedType("String").Encode(), + }, + }, + }, + "staff_member": schema.ObjectType{ + Description: utils.ToPtr("A staff member"), + Fields: schema.ObjectTypeFields{ + "first_name": schema.ObjectField{ + Description: utils.ToPtr("The staff member's first name"), + Type: schema.NewNamedType("String").Encode(), + }, + "last_name": schema.ObjectField{ + Description: utils.ToPtr("The staff member's last name"), + Type: schema.NewNamedType("String").Encode(), + }, + "specialities": schema.ObjectField{ + Description: utils.ToPtr("The staff member's specialities"), + Type: schema.NewArrayType(schema.NewNamedType("String")).Encode(), + }, + }, + }, + }, + Collections: []schema.CollectionInfo{ + { + Name: "articles", + Description: utils.ToPtr("A collection of articles"), + Arguments: schema.CollectionInfoArguments{}, + Type: "article", + UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{ + "ArticleByID": schema.UniquenessConstraint{ + UniqueColumns: []string{"id"}, + }, + }, + ForeignKeys: schema.CollectionInfoForeignKeys{ + "Article_AuthorID": schema.ForeignKeyConstraint{ + ColumnMapping: schema.ForeignKeyConstraintColumnMapping{ + "author_id": "id", + }, + ForeignCollection: "authors", + }, + }, + }, + { + Name: "authors", + Description: utils.ToPtr("A collection of authors"), + Arguments: schema.CollectionInfoArguments{}, + Type: "author", + UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{ + "AuthorByID": schema.UniquenessConstraint{ + UniqueColumns: []string{"id"}, + }, + }, + ForeignKeys: schema.CollectionInfoForeignKeys{}, + }, + { + Name: "institutions", + Description: utils.ToPtr("A collection of institutions"), + Arguments: schema.CollectionInfoArguments{}, + Type: "institution", + UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{ + "InstitutionByID": schema.UniquenessConstraint{ + UniqueColumns: []string{"id"}, + }, + }, + ForeignKeys: schema.CollectionInfoForeignKeys{}, + }, + { + Name: "articles_by_author", + Description: utils.ToPtr("Articles parameterized by author"), + Arguments: schema.CollectionInfoArguments{ + "author_id": schema.ArgumentInfo{ + Type: schema.NewNamedType("Int").Encode(), + }, + }, + Type: "article", + UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{}, + ForeignKeys: schema.CollectionInfoForeignKeys{}, + }, + }, + Functions: []schema.FunctionInfo{ + { + Name: "latest_article_id", + Description: utils.ToPtr("Get the ID of the most recent article"), + Arguments: schema.FunctionInfoArguments{}, + ResultType: schema.NewNullableNamedType("Int").Encode(), + }, + { + Name: "latest_article", + Description: utils.ToPtr("Get the most recent article"), + Arguments: schema.FunctionInfoArguments{}, + ResultType: schema.NewNullableNamedType("article").Encode(), + }, + }, + Procedures: []schema.ProcedureInfo{ + { + Name: "upsert_article", + Description: utils.ToPtr("Insert or update an article"), + Arguments: schema.ProcedureInfoArguments{ + "article": schema.ArgumentInfo{ + Description: utils.ToPtr("The article to insert or update"), + Type: schema.NewNamedType("article").Encode(), + }, + }, + ResultType: schema.NewNullableNamedType("article").Encode(), + }, + { + Name: "delete_articles", + Description: utils.ToPtr("Delete articles which match a predicate"), + Arguments: schema.ProcedureInfoArguments{ + "where": schema.ArgumentInfo{ + Description: utils.ToPtr("The predicate"), + Type: schema.NewPredicateType("article").Encode(), + }, + }, + ResultType: schema.NewArrayType(schema.NewNamedType("article")).Encode(), + }, + }, +} + type Connector struct{} func (mc *Connector) ParseConfiguration(ctx context.Context, rawConfiguration string) (*Configuration, error) { @@ -131,233 +352,33 @@ func (mc *Connector) GetCapabilities(configuration *Configuration) schema.Capabi } func (mc *Connector) GetSchema(ctx context.Context, configuration *Configuration, state *State) (schema.SchemaResponseMarshaler, error) { - return &schema.SchemaResponse{ - ScalarTypes: schema.SchemaResponseScalarTypes{ - "Int": schema.ScalarType{ - AggregateFunctions: schema.ScalarTypeAggregateFunctions{ - "max": schema.AggregateFunctionDefinition{ - ResultType: schema.NewNullableNamedType("Int").Encode(), - }, - "min": schema.AggregateFunctionDefinition{ - ResultType: schema.NewNullableNamedType("Int").Encode(), - }, - }, - ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{ - "eq": schema.NewComparisonOperatorEqual().Encode(), - "in": schema.NewComparisonOperatorIn().Encode(), - }, - Representation: schema.NewTypeRepresentationInteger().Encode(), - }, - "String": { - AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, - ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{ - "eq": schema.NewComparisonOperatorEqual().Encode(), - "in": schema.NewComparisonOperatorIn().Encode(), - "like": schema.NewComparisonOperatorCustom(schema.NewNamedType("String")).Encode(), - }, - Representation: schema.NewTypeRepresentationString().Encode(), - }, - }, - ObjectTypes: schema.SchemaResponseObjectTypes{ - "article": schema.ObjectType{ - Description: utils.ToPtr("An article"), - Fields: schema.ObjectTypeFields{ - "author_id": schema.ObjectField{ - Description: utils.ToPtr("The article's author ID"), - Type: schema.NewNamedType("Int").Encode(), - }, - "id": { - Description: utils.ToPtr("The article's primary key"), - Type: schema.NewNamedType("Int").Encode(), - }, - "title": { - Description: utils.ToPtr("The article's title"), - Type: schema.NewNamedType("String").Encode(), - }, - }, - }, - "author": schema.ObjectType{ - Description: utils.ToPtr("An author"), - Fields: schema.ObjectTypeFields{ - "first_name": { - Description: utils.ToPtr("The author's first name"), - Type: schema.NewNamedType("String").Encode(), - }, - "id": { - Description: utils.ToPtr("The author's primary key"), - Type: schema.NewNamedType("Int").Encode(), - }, - "last_name": { - Description: utils.ToPtr("The author's last name"), - Type: schema.NewNamedType("String").Encode(), - }, - }, - }, - "institution": schema.ObjectType{ - Description: utils.ToPtr("An institution"), - Fields: schema.ObjectTypeFields{ - "departments": schema.ObjectField{ - Description: utils.ToPtr("The institution's departments"), - Type: schema.NewArrayType(schema.NewNamedType("String")).Encode(), - }, - "id": schema.ObjectField{ - Description: utils.ToPtr("The institution's primary key"), - Type: schema.NewNamedType("Int").Encode(), - }, - "location": schema.ObjectField{ - Description: utils.ToPtr("The institution's location"), - Type: schema.NewNamedType("location").Encode(), - }, - "name": schema.ObjectField{ - Description: utils.ToPtr("The institution's name"), - Type: schema.NewNamedType("String").Encode(), - }, - "staff": schema.ObjectField{ - Description: utils.ToPtr("The institution's staff"), - Type: schema.NewArrayType(schema.NewNamedType("staff_member")).Encode(), - }, - }, - }, - "location": schema.ObjectType{ - Description: utils.ToPtr("A location"), - Fields: schema.ObjectTypeFields{ - "campuses": schema.ObjectField{ - Description: utils.ToPtr("The location's campuses"), - Type: schema.NewArrayType(schema.NewNamedType("String")).Encode(), - }, - "city": schema.ObjectField{ - Description: utils.ToPtr("The location's city"), - Type: schema.NewNamedType("String").Encode(), - }, - "country": schema.ObjectField{ - Description: utils.ToPtr("The location's country"), - Type: schema.NewNamedType("String").Encode(), - }, - }, - }, - "staff_member": schema.ObjectType{ - Description: utils.ToPtr("A staff member"), - Fields: schema.ObjectTypeFields{ - "first_name": schema.ObjectField{ - Description: utils.ToPtr("The staff member's first name"), - Type: schema.NewNamedType("String").Encode(), - }, - "last_name": schema.ObjectField{ - Description: utils.ToPtr("The staff member's last name"), - Type: schema.NewNamedType("String").Encode(), - }, - "specialities": schema.ObjectField{ - Description: utils.ToPtr("The staff member's specialities"), - Type: schema.NewArrayType(schema.NewNamedType("String")).Encode(), - }, - }, - }, - }, - Collections: []schema.CollectionInfo{ - { - Name: "articles", - Description: utils.ToPtr("A collection of articles"), - Arguments: schema.CollectionInfoArguments{}, - Type: "article", - UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{ - "ArticleByID": schema.UniquenessConstraint{ - UniqueColumns: []string{"id"}, - }, - }, - ForeignKeys: schema.CollectionInfoForeignKeys{ - "Article_AuthorID": schema.ForeignKeyConstraint{ - ColumnMapping: schema.ForeignKeyConstraintColumnMapping{ - "author_id": "id", - }, - ForeignCollection: "authors", - }, - }, - }, - { - Name: "authors", - Description: utils.ToPtr("A collection of authors"), - Arguments: schema.CollectionInfoArguments{}, - Type: "author", - UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{ - "AuthorByID": schema.UniquenessConstraint{ - UniqueColumns: []string{"id"}, - }, - }, - ForeignKeys: schema.CollectionInfoForeignKeys{}, - }, - { - Name: "institutions", - Description: utils.ToPtr("A collection of institutions"), - Arguments: schema.CollectionInfoArguments{}, - Type: "institution", - UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{ - "InstitutionByID": schema.UniquenessConstraint{ - UniqueColumns: []string{"id"}, - }, - }, - ForeignKeys: schema.CollectionInfoForeignKeys{}, - }, - { - Name: "articles_by_author", - Description: utils.ToPtr("Articles parameterized by author"), - Arguments: schema.CollectionInfoArguments{ - "author_id": schema.ArgumentInfo{ - Type: schema.NewNamedType("Int").Encode(), - }, - }, - Type: "article", - UniquenessConstraints: schema.CollectionInfoUniquenessConstraints{}, - ForeignKeys: schema.CollectionInfoForeignKeys{}, - }, - }, - Functions: []schema.FunctionInfo{ - { - Name: "latest_article_id", - Description: utils.ToPtr("Get the ID of the most recent article"), - Arguments: schema.FunctionInfoArguments{}, - ResultType: schema.NewNullableNamedType("Int").Encode(), - }, - { - Name: "latest_article", - Description: utils.ToPtr("Get the most recent article"), - Arguments: schema.FunctionInfoArguments{}, - ResultType: schema.NewNullableNamedType("article").Encode(), - }, - }, - Procedures: []schema.ProcedureInfo{ - { - Name: "upsert_article", - Description: utils.ToPtr("Insert or update an article"), - Arguments: schema.ProcedureInfoArguments{ - "article": schema.ArgumentInfo{ - Description: utils.ToPtr("The article to insert or update"), - Type: schema.NewNamedType("article").Encode(), - }, - }, - ResultType: schema.NewNullableNamedType("article").Encode(), - }, - { - Name: "delete_articles", - Description: utils.ToPtr("Delete articles which match a predicate"), - Arguments: schema.ProcedureInfoArguments{ - "where": schema.ArgumentInfo{ - Description: utils.ToPtr("The predicate"), - Type: schema.NewPredicateType("article").Encode(), - }, - }, - ResultType: schema.NewArrayType(schema.NewNamedType("article")).Encode(), - }, - }, - }, nil + return ndcSchema, nil } func (mc *Connector) QueryExplain(ctx context.Context, configuration *Configuration, state *State, request *schema.QueryRequest) (*schema.ExplainResponse, error) { + if !slices.ContainsFunc(ndcSchema.Functions, func(f schema.FunctionInfo) bool { + return f.Name == request.Collection + }) && !slices.ContainsFunc(ndcSchema.Collections, func(f schema.CollectionInfo) bool { + return f.Name == request.Collection + }) { + return nil, schema.UnprocessableContentError(fmt.Sprintf("invalid query name: %s", request.Collection), nil) + } return &schema.ExplainResponse{ Details: schema.ExplainResponseDetails{}, }, nil } func (mc *Connector) MutationExplain(ctx context.Context, configuration *Configuration, state *State, request *schema.MutationRequest) (*schema.ExplainResponse, error) { + if len(request.Operations) == 0 { + return nil, schema.UnprocessableContentError("require at least 1 operation", nil) + } + + if !slices.ContainsFunc(ndcSchema.Procedures, func(f schema.ProcedureInfo) bool { + return f.Name == request.Operations[0].Name + }) { + return nil, schema.UnprocessableContentError(fmt.Sprintf("invalid mutation name: %s", request.Operations[0].Name), nil) + } + return &schema.ExplainResponse{ Details: schema.ExplainResponseDetails{}, }, nil @@ -660,10 +681,13 @@ func executeQuery( } } - return &schema.RowSet{ + result := &schema.RowSet{ Aggregates: aggregates, - Rows: rows, - }, nil + } + if len(rows) > 0 || len(result.Aggregates) == 0 { + result.Rows = rows + } + return result, nil } func sortCollection( @@ -712,11 +736,14 @@ func paginate[R any](collection []R, limit *int, offset *int) []R { if offset != nil { start = *offset } + collectionLength := len(collection) + if collectionLength <= start { + return nil + } if limit == nil { return collection[start:] } - - return collection[start : start+*limit] + return collection[start:int(math.Min(float64(collectionLength), float64(start+*limit)))] } func evalOrderBy( diff --git a/example/reference/connector_test.go b/example/reference/connector_test.go index 824b5c2..6852f9c 100644 --- a/example/reference/connector_test.go +++ b/example/reference/connector_test.go @@ -6,10 +6,10 @@ import ( "fmt" "io" "net/http" - "os" "testing" "github.com/hasura/ndc-sdk-go/connector" + "github.com/hasura/ndc-sdk-go/ndctest" "github.com/hasura/ndc-sdk-go/schema" "gotest.tools/v3/assert" ) @@ -49,14 +49,8 @@ func httpPostJSON(url string, body any) (*http.Response, error) { return http.Post(url, "application/json", bytes.NewBuffer(bodyBytes)) } -func assertHTTPResponseStatus(t *testing.T, name string, res *http.Response, statusCode int) { - if res.StatusCode != statusCode { - t.Errorf("\n%s: expected status %d, got %d", name, statusCode, res.StatusCode) - t.FailNow() - } -} - func assertHTTPResponse[B any](t *testing.T, res *http.Response, statusCode int, expectedBody B) { + defer res.Body.Close() bodyBytes, err := io.ReadAll(res.Body) if err != nil { t.Error("failed to read response body") @@ -77,76 +71,19 @@ func assertHTTPResponse[B any](t *testing.T, res *http.Response, statusCode int, assert.DeepEqual(t, expectedBody, body) } -func TestGeneralMethods(t *testing.T) { - server := createTestServer(t).BuildTestServer() - t.Run("capabilities", func(t *testing.T) { - expectedBytes, err := os.ReadFile("./testdata/capabilities") - if err != nil { - t.Errorf("failed to get expected capabilities: %s", err.Error()) - t.FailNow() - } - - var expectedResult schema.CapabilitiesResponse - err = json.Unmarshal(expectedBytes, &expectedResult) - if err != nil { - t.Errorf("failed to read expected body: %s", err.Error()) - t.FailNow() - } - - httpResp, err := http.Get(fmt.Sprintf("%s/capabilities", server.URL)) - if err != nil { - t.Errorf("failed to fetch capabilities: %s", err.Error()) - t.FailNow() - } - - assertHTTPResponse(t, httpResp, http.StatusOK, expectedResult) - }) - - t.Run("schema", func(t *testing.T) { - expectedBytes, err := os.ReadFile("./testdata/schema") - if err != nil { - t.Errorf("failed to fetch expected schema: %s", err.Error()) - t.FailNow() - } - - var expectedSchema schema.SchemaResponse - err = json.Unmarshal(expectedBytes, &expectedSchema) - if err != nil { - t.Errorf("failed to read expected body: %s", err.Error()) - t.FailNow() - } - - httpResp, err := http.Get(fmt.Sprintf("%s/schema", server.URL)) - if err != nil { - t.Errorf("failed to fetch schema: %s", err.Error()) - t.FailNow() - } - - assertHTTPResponse(t, httpResp, http.StatusOK, expectedSchema) - }) - - t.Run("GET /health", func(t *testing.T) { - res, err := http.Get(fmt.Sprintf("%s/health", server.URL)) - if err != nil { - t.Errorf("expected no error, got %s", err) - t.FailNow() - } - assertHTTPResponseStatus(t, "GET /health", res, http.StatusOK) +func TestConnector(t *testing.T) { + ndctest.TestConnector(t, &Connector{}, ndctest.TestConnectorOptions{ + Configuration: "{}", + InlineConfig: true, + TestDataDir: "testdata", }) +} - t.Run("GET /metrics", func(t *testing.T) { - res, err := http.Get(fmt.Sprintf("%s/metrics", server.URL)) - if err != nil { - t.Errorf("expected no error, got %s", err) - t.FailNow() - } - if res.StatusCode != http.StatusNotFound { - t.Errorf("\n%s: expected 404 got status %d", "/metrics", res.StatusCode) - t.FailNow() - } - }) +func TestExplain(t *testing.T) { + server := createTestServer(t).BuildTestServer() + defer server.Close() - t.Run("POST /query/explain", func(t *testing.T) { + t.Run("query_explain_success", func(t *testing.T) { res, err := httpPostJSON(fmt.Sprintf("%s/query/explain", server.URL), schema.QueryRequest{ Collection: "articles", Arguments: schema.QueryRequestArguments{}, @@ -154,33 +91,64 @@ func TestGeneralMethods(t *testing.T) { Query: schema.Query{}, Variables: []schema.QueryRequestVariablesElem{}, }) - if err != nil { - t.Errorf("expected no error, got %s", err) - t.FailNow() - } + assert.NilError(t, err) assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ Details: schema.ExplainResponseDetails{}, }) }) - t.Run("POST /mutation/explain", func(t *testing.T) { + t.Run("query_explain_invalid", func(t *testing.T) { + res, err := httpPostJSON(fmt.Sprintf("%s/query/explain", server.URL), schema.QueryRequest{ + Collection: "test", + Arguments: schema.QueryRequestArguments{}, + CollectionRelationships: schema.QueryRequestCollectionRelationships{}, + Query: schema.Query{}, + Variables: []schema.QueryRequestVariablesElem{}, + }) + assert.NilError(t, err) + assertHTTPResponse(t, res, http.StatusUnprocessableEntity, schema.ErrorResponse{ + Message: "invalid query name: test", + Details: map[string]any{}, + }) + }) + + t.Run("mutation_explain_success", func(t *testing.T) { res, err := httpPostJSON(fmt.Sprintf("%s/mutation/explain", server.URL), schema.MutationRequest{ - Operations: []schema.MutationOperation{}, + Operations: []schema.MutationOperation{ + { + Name: "upsert_article", + Type: schema.MutationOperationProcedure, + }, + }, CollectionRelationships: make(schema.MutationRequestCollectionRelationships), }) - if err != nil { - t.Errorf("expected no error, got %s", err) - t.FailNow() - } + assert.NilError(t, err) assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ Details: schema.ExplainResponseDetails{}, }) }) + t.Run("mutation_explain_invalid", func(t *testing.T) { + res, err := httpPostJSON(fmt.Sprintf("%s/mutation/explain", server.URL), schema.MutationRequest{ + Operations: []schema.MutationOperation{ + { + Name: "test", + Type: schema.MutationOperationProcedure, + }, + }, + CollectionRelationships: make(schema.MutationRequestCollectionRelationships), + }) + assert.NilError(t, err) + assertHTTPResponse(t, res, http.StatusUnprocessableEntity, schema.ErrorResponse{ + Message: "invalid mutation name: test", + Details: map[string]any{}, + }) + }) } func TestQuery(t *testing.T) { server := createTestServer(t).BuildTestServer() + defer server.Close() testCases := []struct { name string @@ -299,9 +267,9 @@ func TestQuery(t *testing.T) { responseURL: fmt.Sprintf("https://raw.githubusercontent.com/hasura/ndc-spec/%s/ndc-reference/tests/query/predicate_with_unrelated_exists/expected.json", test_SpecVersion), }, { - name: "predicate_with_unrelated_exists_and_relationship", - requestURL: fmt.Sprintf("https://raw.githubusercontent.com/hasura/ndc-spec/%s/ndc-reference/tests/query/predicate_with_unrelated_exists_and_relationship/request.json", test_SpecVersion), - response: []byte(`[{"rows":[{"author_if_has_functional_articles":{},"title":"The Next 700 Programming Languages"},{"author_if_has_functional_articles":{"rows":[{"articles":{"rows":[{"title":"Why Functional Programming Matters"},{"title":"The Design And Implementation Of Programming Languages"}]},"first_name":"John","last_name":"Hughes"}]},"title":"Why Functional Programming Matters"},{"author_if_has_functional_articles":{"rows":[{"articles":{"rows":[{"title":"Why Functional Programming Matters"},{"title":"The Design And Implementation Of Programming Languages"}]},"first_name":"John","last_name":"Hughes"}]},"title":"The Design And Implementation Of Programming Languages"}]}]`), + name: "predicate_with_unrelated_exists_and_relationship", + requestURL: fmt.Sprintf("https://raw.githubusercontent.com/hasura/ndc-spec/%s/ndc-reference/tests/query/predicate_with_unrelated_exists_and_relationship/request.json", test_SpecVersion), + responseURL: fmt.Sprintf("https://raw.githubusercontent.com/hasura/ndc-spec/%s/ndc-reference/tests/query/predicate_with_unrelated_exists_and_relationship/expected.json", test_SpecVersion), }, { name: "star_count", @@ -384,6 +352,7 @@ func TestQuery(t *testing.T) { func TestMutation(t *testing.T) { server := createTestServer(t).BuildTestServer() + defer server.Close() testCases := []struct { name string diff --git a/example/reference/go.mod.build b/example/reference/go.mod.build index 0495d3a..90a7dea 100644 --- a/example/reference/go.mod.build +++ b/example/reference/go.mod.build @@ -3,5 +3,5 @@ module github.com/hasura/ndc-sdk-go-reference go 1.21 require ( - github.com/hasura/ndc-sdk-go v1.3.0 + github.com/hasura/ndc-sdk-go v1.4.0 ) diff --git a/schema/query.go b/schema/query.go index 73e82e3..1c780b3 100644 --- a/schema/query.go +++ b/schema/query.go @@ -6,6 +6,19 @@ import ( "fmt" ) +// MarshalJSON implements json.Marshaler. +func (j RowSet) MarshalJSON() ([]byte, error) { + result := map[string]any{} + if len(j.Aggregates) > 0 { + result["aggregates"] = j.Aggregates + } + if j.Rows != nil { + result["rows"] = j.Rows + } + + return json.Marshal(result) +} + // UnmarshalJSONMap decodes FunctionInfo from a JSON map. func (j *FunctionInfo) UnmarshalJSONMap(raw map[string]json.RawMessage) error { rawArguments, ok := raw["arguments"]