Skip to content

Commit

Permalink
add sendHttpRequest mutation
Browse files Browse the repository at this point in the history
  • Loading branch information
hgiasac committed Dec 15, 2024
1 parent 367d03a commit f1293b7
Show file tree
Hide file tree
Showing 36 changed files with 1,299 additions and 81 deletions.
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
FROM golang:1.23 AS builder

WORKDIR /app

ARG VERSION
COPY ndc-http-schema ./ndc-http-schema
COPY go.mod go.sum go.work ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -v -o ndc-cli ./server

RUN CGO_ENABLED=0 go build \
-ldflags "-X github.com/hasura/ndc-http/ndc-http-schema/version.BuildVersion=${VERSION}" \
-v -o ndc-cli ./server

# stage 2: production image
FROM gcr.io/distroless/static-debian12:nonroot
Expand Down
14 changes: 8 additions & 6 deletions connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ import (

"github.com/hasura/ndc-http/connector/internal"
"github.com/hasura/ndc-http/ndc-http-schema/configuration"
rest "github.com/hasura/ndc-http/ndc-http-schema/schema"
"github.com/hasura/ndc-sdk-go/connector"
"github.com/hasura/ndc-sdk-go/schema"
)

// HTTPConnector implements the SDK interface of NDC specification
type HTTPConnector struct {
config *configuration.Configuration
metadata internal.MetadataCollection
capabilities *schema.RawCapabilitiesResponse
rawSchema *schema.RawSchemaResponse
httpClient *http.Client
upstreams *internal.UpstreamManager
config *configuration.Configuration
metadata internal.MetadataCollection
capabilities *schema.RawCapabilitiesResponse
rawSchema *schema.RawSchemaResponse
httpClient *http.Client
upstreams *internal.UpstreamManager
procSendHttpRequest rest.OperationInfo
}

// NewHTTPConnector creates a HTTP connector instance
Expand Down
26 changes: 18 additions & 8 deletions connector/internal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ func (client *HTTPClient) doRequest(ctx context.Context, request *RetryableReque
attribute.String("network.protocol.name", "http"),
)

var namespace string
if client.requests.Schema != nil {
namespace = client.requests.Schema.Name
span.SetAttributes(attribute.String("db.namespace", namespace))
}

if request.ContentLength > 0 {
span.SetAttributes(attribute.Int64("http.request.body.size", request.ContentLength))
}
Expand All @@ -301,8 +307,7 @@ func (client *HTTPClient) doRequest(ctx context.Context, request *RetryableReque
setHeaderAttributes(span, "http.request.header.", request.Headers)

client.propagator.Inject(ctx, propagation.HeaderCarrier(request.Headers))

resp, cancel, err := client.manager.ExecuteRequest(ctx, request, client.requests.Schema.Name)
resp, cancel, err := client.manager.ExecuteRequest(ctx, request, namespace)
if err != nil {
span.SetStatus(codes.Error, "error happened when executing the request")
span.RecordError(err)
Expand Down Expand Up @@ -371,7 +376,7 @@ func (client *HTTPClient) evalHTTPResponse(ctx context.Context, span trace.Span,

var result any
switch {
case strings.HasPrefix(contentType, "text/"):
case strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/svg"):
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil)
Expand Down Expand Up @@ -412,13 +417,18 @@ func (client *HTTPClient) evalHTTPResponse(ctx context.Context, span trace.Span,
}
}

responseType, extractErr := client.extractResultType(resultType)
if extractErr != nil {
return nil, nil, extractErr
var err error
if client.requests.Schema == nil || client.requests.Schema.NDCHttpSchema == nil {
err = json.NewDecoder(resp.Body).Decode(&result)
} else {
responseType, extractErr := client.extractResultType(resultType)
if extractErr != nil {
return nil, nil, extractErr
}

result, err = contenttype.NewJSONDecoder(client.requests.Schema.NDCHttpSchema).Decode(resp.Body, responseType)
}

var err error
result, err = contenttype.NewJSONDecoder(client.requests.Schema.NDCHttpSchema).Decode(resp.Body, responseType)
if err != nil {
return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil)
}
Expand Down
51 changes: 51 additions & 0 deletions connector/internal/contenttype/multipart.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (mfb *MultipartFormEncoder) evalMultipartForm(w *MultipartWriter, bodyInfo
if !ok {
return nil
}

switch bodyType := bodyInfo.Type.Interface().(type) {
case *schema.NullableType:
return mfb.evalMultipartForm(w, &rest.ArgumentInfo{
Expand All @@ -73,6 +74,7 @@ func (mfb *MultipartFormEncoder) evalMultipartForm(w *MultipartWriter, bodyInfo
if !ok {
break
}

kind := bodyData.Kind()
switch kind {
case reflect.Map, reflect.Interface:
Expand Down Expand Up @@ -276,3 +278,52 @@ func (mfb *MultipartFormEncoder) evalEncodingHeaders(encHeaders map[string]rest.

return results, nil
}

// EncodeArbitrary encodes the unknown data to multipart/form.
func (c *MultipartFormEncoder) EncodeArbitrary(bodyData any) (*bytes.Reader, string, error) {
buffer := new(bytes.Buffer)
writer := NewMultipartWriter(buffer)

reflectValue, ok := utils.UnwrapPointerFromReflectValue(reflect.ValueOf(bodyData))
if ok {
valueMap, ok := reflectValue.Interface().(map[string]any)
if !ok {
return nil, "", fmt.Errorf("invalid body for multipart/form, expected object, got: %s", reflectValue.Kind())
}

for key, value := range valueMap {
if err := c.evalFormDataReflection(writer, key, reflect.ValueOf(value)); err != nil {
return nil, "", fmt.Errorf("invalid body for multipart/form, %s: %w", key, err)
}
}
}

if err := writer.Close(); err != nil {
return nil, "", err
}

reader := bytes.NewReader(buffer.Bytes())
buffer.Reset()

return reader, writer.FormDataContentType(), nil
}

func (c *MultipartFormEncoder) evalFormDataReflection(w *MultipartWriter, key string, reflectValue reflect.Value) error {
reflectValue, ok := utils.UnwrapPointerFromReflectValue(reflectValue)
if !ok {
return nil
}

kind := reflectValue.Kind()
switch kind {
case reflect.Map, reflect.Struct, reflect.Array, reflect.Slice:
return w.WriteJSON(key, reflectValue.Interface(), http.Header{})
default:
value, err := StringifySimpleScalar(reflectValue, kind)
if err != nil {
return err
}

return w.WriteField(key, value, http.Header{})
}
}
34 changes: 30 additions & 4 deletions connector/internal/contenttype/url_encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,51 @@ func NewURLParameterEncoder(schema *rest.NDCHttpSchema, contentType string) *URL
}
}

func (c *URLParameterEncoder) Encode(bodyInfo *rest.ArgumentInfo, bodyData any) (io.ReadSeeker, error) {
func (c *URLParameterEncoder) Encode(bodyInfo *rest.ArgumentInfo, bodyData any) (io.ReadSeeker, int64, error) {
queryParams, err := c.EncodeParameterValues(&rest.ObjectField{
ObjectField: schema.ObjectField{
Type: bodyInfo.Type,
},
HTTP: bodyInfo.HTTP.Schema,
}, reflect.ValueOf(bodyData), []string{"body"})
if err != nil {
return nil, err
return nil, 0, err
}

if len(queryParams) == 0 {
return nil, nil
return nil, 0, nil
}
q := url.Values{}
for _, qp := range queryParams {
keys := qp.Keys()
EvalQueryParameterURL(&q, "", bodyInfo.HTTP.EncodingObject, keys, qp.Values())
}
rawQuery := EncodeQueryValues(q, true)
result := bytes.NewReader([]byte(rawQuery))

return bytes.NewReader([]byte(rawQuery)), nil
return result, result.Size(), nil
}

// Encode marshals the arbitrary body to xml bytes.
func (c *URLParameterEncoder) EncodeArbitrary(bodyData any) (io.ReadSeeker, int64, error) {
queryParams, err := c.encodeParameterReflectionValues(reflect.ValueOf(bodyData), []string{"body"})
if err != nil {
return nil, 0, err
}

if len(queryParams) == 0 {
return nil, 0, nil
}
q := url.Values{}
encObject := rest.EncodingObject{}
for _, qp := range queryParams {
keys := qp.Keys()
EvalQueryParameterURL(&q, "", encObject, keys, qp.Values())
}
rawQuery := EncodeQueryValues(q, true)
result := bytes.NewReader([]byte(rawQuery))

return result, result.Size(), nil
}

func (c *URLParameterEncoder) EncodeParameterValues(objectField *rest.ObjectField, reflectValue reflect.Value, fieldPaths []string) (ParameterItems, error) {
Expand Down Expand Up @@ -382,6 +405,7 @@ func buildParamQueryKey(name string, encObject rest.EncodingObject, keys Keys, v
return strings.Join(resultKeys, "")
}

// EvalQueryParameterURL evaluate the query parameter URL
func EvalQueryParameterURL(q *url.Values, name string, encObject rest.EncodingObject, keys Keys, values []string) {
if len(values) == 0 {
return
Expand Down Expand Up @@ -411,6 +435,7 @@ func EvalQueryParameterURL(q *url.Values, name string, encObject rest.EncodingOb
}
}

// EncodeQueryValues encode query values to string.
func EncodeQueryValues(qValues url.Values, allowReserved bool) string {
if !allowReserved {
return qValues.Encode()
Expand All @@ -433,6 +458,7 @@ func EncodeQueryValues(qValues url.Values, allowReserved bool) string {
return builder.String()
}

// SetHeaderParameters set parameters to request headers
func SetHeaderParameters(header *http.Header, param *rest.RequestParameter, queryParams ParameterItems) {
defaultParam := queryParams.FindDefault()
// the param is an array
Expand Down
2 changes: 1 addition & 1 deletion connector/internal/contenttype/url_encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ func TestCreateFormURLEncoded(t *testing.T) {
assert.NilError(t, json.Unmarshal([]byte(tc.RawArguments), &arguments))
argumentInfo := info.Arguments["body"]
builder := NewURLParameterEncoder(ndcSchema, rest.ContentTypeFormURLEncoded)
buf, err := builder.Encode(&argumentInfo, arguments["body"])
buf, _, err := builder.Encode(&argumentInfo, arguments["body"])
assert.NilError(t, err)
result, err := io.ReadAll(buf)
assert.NilError(t, err)
Expand Down
4 changes: 4 additions & 0 deletions connector/internal/contenttype/xml_decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func (c *XMLDecoder) Decode(r io.Reader, resultType schema.Type) (any, error) {
return nil, fmt.Errorf("failed to decode the xml result: %w", err)
}

if c.schema == nil {
return decodeArbitraryXMLBlock(xmlTree), nil
}

result, err := c.evalXMLField(xmlTree, "", rest.ObjectField{
ObjectField: schema.ObjectField{
Type: resultType,
Expand Down
Loading

0 comments on commit f1293b7

Please sign in to comment.