From 2a9d1bbc5595283d232acca25a737e36ff68cb84 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Thu, 30 May 2024 00:40:14 +0700 Subject: [PATCH] Support ndc-spec v0.1.3 (#105) (#106) --- README.md | 2 +- .../templates/new/connector.go.tmpl | 2 +- .../testdata/basic/source/connector.go | 2 +- .../testdata/empty/source/connector.go | 2 +- example/codegen/connector.go | 2 +- example/codegen/testdata/capabilities | 2 +- example/reference/connector_test.go | 2 +- schema/extend.go | 196 +++++++++++++++++- schema/schema_test.go | 69 ++++++ 9 files changed, 271 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0bcc02d..a8ffbb3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ All functions of the Connector interface are analogous to their Rust counterpart ## Features -The SDK fully supports [NDC Specification v0.1.2](https://hasura.github.io/ndc-spec/specification/changelog.html#012) and [Connector Deployment spec](https://github.com/hasura/ndc-hub/blob/main/rfcs/0000-deployment.md) with following features: +The SDK fully supports [NDC Specification v0.1.3](https://hasura.github.io/ndc-spec/specification/changelog.html#013) and [Connector Deployment spec](https://github.com/hasura/ndc-hub/blob/main/rfcs/0000-deployment.md) with following features: - Connector HTTP server - Authentication diff --git a/cmd/hasura-ndc-go/templates/new/connector.go.tmpl b/cmd/hasura-ndc-go/templates/new/connector.go.tmpl index 7ee5059..35687a1 100644 --- a/cmd/hasura-ndc-go/templates/new/connector.go.tmpl +++ b/cmd/hasura-ndc-go/templates/new/connector.go.tmpl @@ -9,7 +9,7 @@ import ( ) var connectorCapabilities = schema.CapabilitiesResponse{ - Version: "0.1.2", + Version: "0.1.3", Capabilities: schema.Capabilities{ Query: schema.QueryCapabilities{ Variables: schema.LeafCapability{}, diff --git a/cmd/hasura-ndc-go/testdata/basic/source/connector.go b/cmd/hasura-ndc-go/testdata/basic/source/connector.go index 93f7119..9aba204 100644 --- a/cmd/hasura-ndc-go/testdata/basic/source/connector.go +++ b/cmd/hasura-ndc-go/testdata/basic/source/connector.go @@ -9,7 +9,7 @@ import ( ) var connectorCapabilities = schema.CapabilitiesResponse{ - Version: "0.1.2", + Version: "0.1.3", Capabilities: schema.Capabilities{ Query: schema.QueryCapabilities{ Variables: schema.LeafCapability{}, diff --git a/cmd/hasura-ndc-go/testdata/empty/source/connector.go b/cmd/hasura-ndc-go/testdata/empty/source/connector.go index e220d70..52ba12d 100644 --- a/cmd/hasura-ndc-go/testdata/empty/source/connector.go +++ b/cmd/hasura-ndc-go/testdata/empty/source/connector.go @@ -9,7 +9,7 @@ import ( ) var connectorCapabilities = schema.CapabilitiesResponse{ - Version: "0.1.2", + Version: "0.1.3", Capabilities: schema.Capabilities{ Query: schema.QueryCapabilities{ Variables: schema.LeafCapability{}, diff --git a/example/codegen/connector.go b/example/codegen/connector.go index 715e191..a98dec7 100644 --- a/example/codegen/connector.go +++ b/example/codegen/connector.go @@ -9,7 +9,7 @@ import ( ) var connectorCapabilities = schema.CapabilitiesResponse{ - Version: "0.1.2", + Version: "0.1.3", Capabilities: schema.Capabilities{ Query: schema.QueryCapabilities{ Variables: schema.LeafCapability{}, diff --git a/example/codegen/testdata/capabilities b/example/codegen/testdata/capabilities index 9ca52fb..4b33a14 100644 --- a/example/codegen/testdata/capabilities +++ b/example/codegen/testdata/capabilities @@ -1,5 +1,5 @@ { - "version": "0.1.2", + "version": "0.1.3", "capabilities": { "query": { "variables": {} diff --git a/example/reference/connector_test.go b/example/reference/connector_test.go index 3646dee..2d0d884 100644 --- a/example/reference/connector_test.go +++ b/example/reference/connector_test.go @@ -14,7 +14,7 @@ import ( "github.com/hasura/ndc-sdk-go/schema" ) -const test_SpecVersion = "v0.1.2" +const test_SpecVersion = "v0.1.3" func createTestServer(t *testing.T) *connector.Server[Configuration, State] { server, err := connector.NewServer[Configuration, State](&Connector{}, &connector.ServerOptions{ diff --git a/schema/extend.go b/schema/extend.go index f4f9657..c87de91 100644 --- a/schema/extend.go +++ b/schema/extend.go @@ -61,6 +61,22 @@ type Argument struct { Value any `json:"value" yaml:"value" mapstructure:"value"` } +// NewLiteralArgument creates an argument with a literal value +func NewLiteralArgument(value any) *Argument { + return &Argument{ + Type: ArgumentTypeLiteral, + Value: value, + } +} + +// NewVariableArgument creates an argument with a variable name +func NewVariableArgument(name string) *Argument { + return &Argument{ + Type: ArgumentTypeVariable, + Name: name, + } +} + // MarshalJSON implements json.Marshaler. func (j Argument) MarshalJSON() ([]byte, error) { result := map[string]any{ @@ -298,6 +314,14 @@ func (j *Field) UnmarshalJSON(b []byte) error { } results["fields"] = fields } + var arguments map[string]Argument + rawArguments, ok := raw["arguments"] + if ok && !isNullJSON(rawArguments) { + if err = json.Unmarshal(rawArguments, &arguments); err != nil { + return fmt.Errorf("field arguments in Field: %s", err) + } + results["arguments"] = arguments + } case FieldTypeRelationship: relationship, err := unmarshalStringFromJsonMap(raw, "relationship", true) if err != nil { @@ -378,6 +402,15 @@ func (j Field) AsColumn() (*ColumnField, error) { result.Fields = fields } + rawArguments, ok := j["arguments"] + if ok && !isNil(rawArguments) { + arguments, ok := rawArguments.(map[string]Argument) + if !ok { + return nil, fmt.Errorf("invalid ColumnField.arguments type; expected map[string]Argument, got %+v", rawArguments) + } + result.Arguments = arguments + } + return result, nil } @@ -453,6 +486,8 @@ type ColumnField struct { // the caller can request a subset of the complete column data, by specifying fields to fetch here. // If omitted, the column data will be fetched in full. Fields NestedField `json:"fields,omitempty" yaml:"fields,omitempty" mapstructure:"fields"` + + Arguments map[string]Argument `json:"arguments,omitempty" yaml:"arguments,omitempty" mapstructure:"fields"` } // Encode converts the instance to raw Field @@ -465,6 +500,9 @@ func (f ColumnField) Encode() Field { if len(f.Fields) > 0 { r["fields"] = f.Fields } + if len(f.Arguments) > 0 { + r["arguments"] = f.Arguments + } return r } @@ -481,6 +519,13 @@ func NewColumnField(column string, fields NestedFieldEncoder) *ColumnField { } } +// NewColumnFieldWithArguments creates a new ColumnField instance with arguments +func NewColumnFieldWithArguments(column string, fields NestedFieldEncoder, arguments map[string]Argument) *ColumnField { + cf := NewColumnField(column, fields) + cf.Arguments = arguments + return cf +} + // RelationshipField represents a relationship field type RelationshipField struct { Type FieldType `json:"type" yaml:"type" mapstructure:"type"` @@ -564,6 +609,25 @@ type ComparisonTarget struct { FieldPath []string `json:"field_path,omitempty" yaml:"field_path,omitempty" mapstructure:"field_path"` } +// NewComparisonTargetColumn creates a ComparisonTarget with column type +func NewComparisonTargetColumn(name string, fieldPath []string, path []PathElement) *ComparisonTarget { + return &ComparisonTarget{ + Type: ComparisonTargetTypeColumn, + Name: name, + Path: path, + FieldPath: fieldPath, + } +} + +// NewComparisonTargetRootCollectionColumn creates a ComparisonTarget with root_collection_column type +func NewComparisonTargetRootCollectionColumn(name string, fieldPath []string) *ComparisonTarget { + return &ComparisonTarget{ + Type: ComparisonTargetTypeRootCollectionColumn, + Name: name, + FieldPath: fieldPath, + } +} + // ExpressionType represents the filtering expression enums type ExpressionType string @@ -843,6 +907,14 @@ type ComparisonValueColumn struct { Column ComparisonTarget `json:"column" yaml:"column" mapstructure:"column"` } +// NewComparisonValueColumn creates a new ComparisonValueColumn instance +func NewComparisonValueColumn(column ComparisonTarget) *ComparisonValueColumn { + return &ComparisonValueColumn{ + Type: ComparisonValueTypeColumn, + Column: column, + } +} + // Encode converts to the raw comparison value func (cv ComparisonValueColumn) Encode() ComparisonValue { return map[string]any{ @@ -857,6 +929,14 @@ type ComparisonValueScalar struct { Value any `json:"value" yaml:"value" mapstructure:"value"` } +// NewComparisonValueScalar creates a new ComparisonValueScalar instance +func NewComparisonValueScalar(value any) *ComparisonValueScalar { + return &ComparisonValueScalar{ + Type: ComparisonValueTypeScalar, + Value: value, + } +} + // Encode converts to the raw comparison value func (cv ComparisonValueScalar) Encode() ComparisonValue { return map[string]any{ @@ -871,6 +951,14 @@ type ComparisonValueVariable struct { Name string `json:"name" yaml:"name" mapstructure:"name"` } +// NewComparisonValueVariable creates a new ComparisonValueVariable instance +func NewComparisonValueVariable(name string) *ComparisonValueVariable { + return &ComparisonValueVariable{ + Type: ComparisonValueTypeVariable, + Name: name, + } +} + // Encode converts to the raw comparison value func (cv ComparisonValueVariable) Encode() ComparisonValue { return map[string]any{ @@ -1510,6 +1598,21 @@ type ExpressionAnd struct { Expressions []Expression `json:"expressions" yaml:"expressions" mapstructure:"expressions"` } +// NewExpressionAnd creates an ExpressionAnd instance +func NewExpressionAnd(expressions ...ExpressionEncoder) *ExpressionAnd { + exprs := make([]Expression, len(expressions)) + for i, expr := range expressions { + if expr == nil { + continue + } + exprs[i] = expr.Encode() + } + return &ExpressionAnd{ + Type: ExpressionTypeAnd, + Expressions: exprs, + } +} + // Encode converts the instance to a raw Expression func (exp ExpressionAnd) Encode() Expression { return Expression{ @@ -1526,6 +1629,21 @@ type ExpressionOr struct { Expressions []Expression `json:"expressions" yaml:"expressions" mapstructure:"expressions"` } +// NewExpressionOr creates an ExpressionOr instance +func NewExpressionOr(expressions ...ExpressionEncoder) *ExpressionOr { + exprs := make([]Expression, len(expressions)) + for i, expr := range expressions { + if expr == nil { + continue + } + exprs[i] = expr.Encode() + } + return &ExpressionOr{ + Type: ExpressionTypeOr, + Expressions: exprs, + } +} + // Encode converts the instance to a raw Expression func (exp ExpressionOr) Encode() Expression { return Expression{ @@ -1542,6 +1660,17 @@ type ExpressionNot struct { Expression Expression `json:"expression" yaml:"expression" mapstructure:"expression"` } +// NewExpressionNot creates an ExpressionNot instance +func NewExpressionNot(expression ExpressionEncoder) *ExpressionNot { + result := &ExpressionNot{ + Type: ExpressionTypeNot, + } + if expression != nil { + result.Expression = expression.Encode() + } + return result +} + // Encode converts the instance to a raw Expression func (exp ExpressionNot) Encode() Expression { return Expression{ @@ -1559,6 +1688,15 @@ type ExpressionUnaryComparisonOperator struct { Column ComparisonTarget `json:"column" yaml:"column" mapstructure:"column"` } +// NewExpressionUnaryComparisonOperator creates an ExpressionUnaryComparisonOperator instance +func NewExpressionUnaryComparisonOperator(column ComparisonTarget, operator UnaryComparisonOperator) *ExpressionUnaryComparisonOperator { + return &ExpressionUnaryComparisonOperator{ + Type: ExpressionTypeUnaryComparisonOperator, + Column: column, + Operator: operator, + } +} + // Encode converts the instance to a raw Expression func (exp ExpressionUnaryComparisonOperator) Encode() Expression { return Expression{ @@ -1578,6 +1716,19 @@ type ExpressionBinaryComparisonOperator struct { Value ComparisonValue `json:"value" yaml:"value" mapstructure:"value"` } +// NewExpressionBinaryComparisonOperator creates an ExpressionBinaryComparisonOperator instance +func NewExpressionBinaryComparisonOperator(column ComparisonTarget, operator string, value ComparisonValueEncoder) *ExpressionBinaryComparisonOperator { + result := &ExpressionBinaryComparisonOperator{ + Type: ExpressionTypeBinaryComparisonOperator, + Column: column, + Operator: operator, + } + if value != nil { + result.Value = value.Encode() + } + return result +} + // Encode converts the instance to a raw Expression func (exp ExpressionBinaryComparisonOperator) Encode() Expression { return Expression{ @@ -1597,6 +1748,21 @@ type ExpressionExists struct { InCollection ExistsInCollection `json:"in_collection" yaml:"in_collection" mapstructure:"in_collection"` } +// NewExpressionExists creates an ExpressionExists instance +func NewExpressionExists(predicate ExpressionEncoder, inCollection ExistsInCollectionEncoder) *ExpressionExists { + result := &ExpressionExists{ + Type: ExpressionTypeExists, + InCollection: inCollection.Encode(), + } + if predicate != nil { + result.Predicate = predicate.Encode() + } + if inCollection != nil { + result.InCollection = inCollection.Encode() + } + return result +} + // Encode converts the instance to a raw Expression func (exp ExpressionExists) Encode() Expression { return Expression{ @@ -2225,7 +2391,17 @@ type OrderByColumn struct { // Any relationships to traverse to reach this column Path []PathElement `json:"path" yaml:"path" mapstructure:"path"` // Any field path to a nested field within the column - FieldPath []string `json:"field_path" yaml:"field_path" mapstructure:"field_path"` + FieldPath []string `json:"field_path,omitempty" yaml:"field_path,omitempty" mapstructure:"field_path"` +} + +// NewOrderByColumn creates an OrderByColumn instance +func NewOrderByColumn(name string, fieldPath []string, path []PathElement) *OrderByColumn { + return &OrderByColumn{ + Type: OrderByTargetTypeColumn, + Name: name, + FieldPath: fieldPath, + Path: path, + } } // Encode converts the instance to raw OrderByTarget @@ -2252,6 +2428,16 @@ type OrderBySingleColumnAggregate struct { Path []PathElement `json:"path" yaml:"path" mapstructure:"path"` } +// NewOrderBySingleColumnAggregate creates an OrderBySingleColumnAggregate instance +func NewOrderBySingleColumnAggregate(column string, function string, path []PathElement) *OrderBySingleColumnAggregate { + return &OrderBySingleColumnAggregate{ + Type: OrderByTargetTypeSingleColumnAggregate, + Column: column, + Function: function, + Path: path, + } +} + // Encode converts the instance to raw OrderByTarget func (ob OrderBySingleColumnAggregate) Encode() OrderByTarget { return OrderByTarget{ @@ -2272,6 +2458,14 @@ type OrderByStarCountAggregate struct { Path []PathElement `json:"path" yaml:"path" mapstructure:"path"` } +// NewOrderByStarCountAggregate creates an OrderByStarCountAggregate instance +func NewOrderByStarCountAggregate(path []PathElement) *OrderByStarCountAggregate { + return &OrderByStarCountAggregate{ + Type: OrderByTargetTypeStarCountAggregate, + Path: path, + } +} + // Encode converts the instance to raw OrderByTarget func (ob OrderByStarCountAggregate) Encode() OrderByTarget { return OrderByTarget{ diff --git a/schema/schema_test.go b/schema/schema_test.go index 74106b9..82a16be 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -113,6 +113,75 @@ func TestQueryRequest(t *testing.T) { }, }, }, + { + "predicate_with_nested_field_eq", + []byte(`{ + "collection": "institutions", + "arguments": {}, + "query": { + "fields": { + "id": { + "type": "column", + "column": "id" + }, + "location": { + "type": "column", + "column": "location", + "fields": { + "type": "object", + "fields": { + "city": { + "type": "column", + "column": "city" + }, + "campuses": { + "type": "column", + "column": "campuses", + "arguments": { + "limit": { + "type": "literal", + "value": null + } + } + } + } + } + } + }, + "predicate": { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "location", + "field_path": ["city"], + "path": [] + }, + "operator": "eq", + "value": { + "type": "scalar", + "value": "London" + } + } + }, + "collection_relationships": {} + }`), + QueryRequest{ + Collection: "institutions", + Query: Query{ + Fields: QueryFields{ + "id": NewColumnField("id", nil).Encode(), + "location": NewColumnField("location", NewNestedObject(map[string]FieldEncoder{ + "city": NewColumnField("city", nil), + "campuses": NewColumnFieldWithArguments("campuses", nil, map[string]Argument{ + "limit": *NewLiteralArgument(nil), + }), + })).Encode(), + }, + Predicate: NewExpressionBinaryComparisonOperator(*NewComparisonTargetColumn("location", []string{"city"}, []PathElement{}), "eq", NewComparisonValueScalar("London")).Encode(), + }, + CollectionRelationships: QueryRequestCollectionRelationships{}, + }, + }, } for _, tc := range testCases {