Repository: hyperledger/firefly (go)

Issue:
Add `fetchreference` param for GET `/events/:id` endpoint
We currently have a `fetchreferences` query param for GET `/events` that will fetch the information of the event referenced in the api. There should be an equivalent query parameter when getting an event by `id`. 

Pull request:
adds `fetchreference` to /events/:id
When provided, firefly will include the record that the `ref` field references in the response body.

closes #1016 

Patch:
diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml
index 39c0b79c4..eaf809ee9 100644
--- a/docs/swagger/swagger.yaml
+++ b/docs/swagger/swagger.yaml
@@ -5249,12 +5249,19 @@ paths:
       operationId: getEvents
       parameters:
       - description: When set, the API will return the record that this item references
-          in its 'ref' field
+          in its 'reference' field
         in: query
         name: fetchreferences
         schema:
           example: "true"
           type: string
+      - description: When set, the API will return the record that this item references
+          in its 'reference' field
+        in: query
+        name: fetchreference
+        schema:
+          example: "true"
+          type: string
       - description: Server-side request timeout (milliseconds, or set a custom suffix
           like 10s)
         in: header
@@ -5433,6 +5440,13 @@ paths:
         required: true
         schema:
           type: string
+      - description: When set, the API will return the record that this item references
+          in its 'reference' field
+        in: query
+        name: fetchreference
+        schema:
+          example: "true"
+          type: string
       - description: Server-side request timeout (milliseconds, or set a custom suffix
           like 10s)
         in: header
@@ -14515,12 +14529,19 @@ paths:
           example: default
           type: string
       - description: When set, the API will return the record that this item references
-          in its 'ref' field
+          in its 'reference' field
         in: query
         name: fetchreferences
         schema:
           example: "true"
           type: string
+      - description: When set, the API will return the record that this item references
+          in its 'reference' field
+        in: query
+        name: fetchreference
+        schema:
+          example: "true"
+          type: string
       - description: Server-side request timeout (milliseconds, or set a custom suffix
           like 10s)
         in: header
@@ -14706,6 +14727,13 @@ paths:
         schema:
           example: default
           type: string
+      - description: When set, the API will return the record that this item references
+          in its 'reference' field
+        in: query
+        name: fetchreference
+        schema:
+          example: "true"
+          type: string
       - description: Server-side request timeout (milliseconds, or set a custom suffix
           like 10s)
         in: header
diff --git a/internal/apiserver/route_get_event_by_id.go b/internal/apiserver/route_get_event_by_id.go
index ce6d154a8..c045ea810 100644
--- a/internal/apiserver/route_get_event_by_id.go
+++ b/internal/apiserver/route_get_event_by_id.go
@@ -18,6 +18,7 @@ package apiserver
 
 import (
 	"net/http"
+	"strings"
 
 	"github.com/hyperledger/firefly-common/pkg/ffapi"
 	"github.com/hyperledger/firefly/internal/coremsgs"
@@ -31,15 +32,19 @@ var getEventByID = &ffapi.Route{
 	PathParams: []*ffapi.PathParam{
 		{Name: "eid", Description: coremsgs.APIParamsEventID},
 	},
-	QueryParams:     nil,
+	QueryParams: []*ffapi.QueryParam{
+		{Name: "fetchreference", Example: "true", Description: coremsgs.APIParamsFetchReference, IsBool: true},
+	},
 	Description:     coremsgs.APIEndpointsGetEventByID,
 	JSONInputValue:  nil,
 	JSONOutputValue: func() interface{} { return &core.Event{} },
 	JSONOutputCodes: []int{http.StatusOK},
 	Extensions: &coreExtensions{
 		CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
-			output, err = cr.or.GetEventByID(cr.ctx, r.PP["eid"])
-			return output, err
+			if strings.EqualFold(r.QP["fetchreference"], "true") {
+				return cr.or.GetEventByIDWithReference(cr.ctx, r.PP["eid"])
+			}
+			return cr.or.GetEventByID(cr.ctx, r.PP["eid"])
 		},
 	},
 }
diff --git a/internal/apiserver/route_get_events.go b/internal/apiserver/route_get_events.go
index 8167b8de8..7d10b7a12 100644
--- a/internal/apiserver/route_get_events.go
+++ b/internal/apiserver/route_get_events.go
@@ -33,6 +33,7 @@ var getEvents = &ffapi.Route{
 	PathParams: nil,
 	QueryParams: []*ffapi.QueryParam{
 		{Name: "fetchreferences", Example: "true", Description: coremsgs.APIParamsFetchReferences, IsBool: true},
+		{Name: "fetchreference", Example: "true", Description: coremsgs.APIParamsFetchReference, IsBool: true},
 	},
 	Description:     coremsgs.APIEndpointsGetEvents,
 	JSONInputValue:  nil,
@@ -41,7 +42,7 @@ var getEvents = &ffapi.Route{
 	Extensions: &coreExtensions{
 		FilterFactory: database.EventQueryFactory,
 		CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
-			if strings.EqualFold(r.QP["fetchreferences"], "true") {
+			if strings.EqualFold(r.QP["fetchreferences"], "true") || strings.EqualFold(r.QP["fetchreference"], "true") {
 				return filterResult(cr.or.GetEventsWithReferences(cr.ctx, cr.filter))
 			}
 			return filterResult(cr.or.GetEvents(cr.ctx, cr.filter))
diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go
index 0895c3a23..7a81972b7 100644
--- a/internal/coremsgs/en_api_translations.go
+++ b/internal/coremsgs/en_api_translations.go
@@ -41,7 +41,8 @@ var (
 	APIParamsDatatypeName                   = ffm("api.params.datatypeName", "The name of the datatype")
 	APIParamsDatatypeVersion                = ffm("api.params.datatypeVersion", "The version of the datatype")
 	APIParamsEventID                        = ffm("api.params.eventID", "The event ID")
-	APIParamsFetchReferences                = ffm("api.params.fetchReferences", "When set, the API will return the record that this item references in its 'ref' field")
+	APIParamsFetchReferences                = ffm("api.params.fetchReferences", "When set, the API will return the record that this item references in its 'reference' field")
+	APIParamsFetchReference                 = ffm("api.params.fetchReference", "When set, the API will return the record that this item references in its 'reference' field")
 	APIParamsGroupHash                      = ffm("api.params.groupID", "The hash of the group")
 	APIParamsFetchVerifiers                 = ffm("api.params.fetchVerifiers", "When set, the API will return the verifier for this identity")
 	APIParamsIdentityID                     = ffm("api.params.identityID", "The identity ID, which is a UUID generated by FireFly")
diff --git a/internal/orchestrator/data_query.go b/internal/orchestrator/data_query.go
index ea67f42e5..dbaea570f 100644
--- a/internal/orchestrator/data_query.go
+++ b/internal/orchestrator/data_query.go
@@ -135,6 +135,18 @@ func (or *orchestrator) GetEventByID(ctx context.Context, id string) (*core.Even
 	return or.database().GetEventByID(ctx, or.namespace.Name, u)
 }
 
+func (or *orchestrator) GetEventByIDWithReference(ctx context.Context, id string) (*core.EnrichedEvent, error) {
+	u, err := fftypes.ParseUUID(ctx, id)
+	if err != nil {
+		return nil, err
+	}
+	event, err := or.database().GetEventByID(ctx, or.namespace.Name, u)
+	if err != nil {
+		return nil, err
+	}
+	return or.events.EnrichEvent(ctx, event)
+}
+
 func (or *orchestrator) GetTransactions(ctx context.Context, filter database.AndFilter) ([]*core.Transaction, *database.FilterResult, error) {
 	return or.database().GetTransactions(ctx, or.namespace.Name, filter)
 }
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index 5d6ba3424..9e537add8 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -103,6 +103,7 @@ type Orchestrator interface {
 	GetOperationByID(ctx context.Context, id string) (*core.Operation, error)
 	GetOperations(ctx context.Context, filter database.AndFilter) ([]*core.Operation, *database.FilterResult, error)
 	GetEventByID(ctx context.Context, id string) (*core.Event, error)
+	GetEventByIDWithReference(ctx context.Context, id string) (*core.EnrichedEvent, error)
 	GetEvents(ctx context.Context, filter database.AndFilter) ([]*core.Event, *database.FilterResult, error)
 	GetEventsWithReferences(ctx context.Context, filter database.AndFilter) ([]*core.EnrichedEvent, *database.FilterResult, error)
 	GetBlockchainEventByID(ctx context.Context, id string) (*core.BlockchainEvent, error)
diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go
index 35e9c8cca..e2e092127 100644
--- a/mocks/orchestratormocks/orchestrator.go
+++ b/mocks/orchestratormocks/orchestrator.go
@@ -515,6 +515,29 @@ func (_m *Orchestrator) GetEventByID(ctx context.Context, id string) (*core.Even
 	return r0, r1
 }
 
+// GetEventByIDWithReference provides a mock function with given fields: ctx, id
+func (_m *Orchestrator) GetEventByIDWithReference(ctx context.Context, id string) (*core.EnrichedEvent, error) {
+	ret := _m.Called(ctx, id)
+
+	var r0 *core.EnrichedEvent
+	if rf, ok := ret.Get(0).(func(context.Context, string) *core.EnrichedEvent); ok {
+		r0 = rf(ctx, id)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*core.EnrichedEvent)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, id)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // GetEvents provides a mock function with given fields: ctx, filter
 func (_m *Orchestrator) GetEvents(ctx context.Context, filter database.AndFilter) ([]*core.Event, *database.FilterResult, error) {
 	ret := _m.Called(ctx, filter)


Test patch:
diff --git a/internal/apiserver/route_get_event_by_id_test.go b/internal/apiserver/route_get_event_by_id_test.go
index 4a189bd7f..72ef9c4d1 100644
--- a/internal/apiserver/route_get_event_by_id_test.go
+++ b/internal/apiserver/route_get_event_by_id_test.go
@@ -38,3 +38,17 @@ func TestGetEventByID(t *testing.T) {
 
 	assert.Equal(t, 200, res.Result().StatusCode)
 }
+
+func TestGetEventByIDWithReference(t *testing.T) {
+	o, r := newTestAPIServer()
+	o.On("Authorize", mock.Anything, mock.Anything).Return(nil)
+	req := httptest.NewRequest("GET", "/api/v1/namespaces/mynamespace/events/abcd12345?fetchreference", nil)
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	res := httptest.NewRecorder()
+
+	o.On("GetEventByIDWithReference", mock.Anything, "abcd12345").
+		Return(&core.EnrichedEvent{}, nil)
+	r.ServeHTTP(res, req)
+
+	assert.Equal(t, 200, res.Result().StatusCode)
+}
diff --git a/internal/apiserver/route_get_events_test.go b/internal/apiserver/route_get_events_test.go
index fb605cc27..400692d7d 100644
--- a/internal/apiserver/route_get_events_test.go
+++ b/internal/apiserver/route_get_events_test.go
@@ -63,3 +63,26 @@ func TestGetEventsWithReferences(t *testing.T) {
 	assert.Equal(t, int64(0), resWithCount.Count)
 	assert.Equal(t, int64(10), resWithCount.Total)
 }
+
+func TestGetEventsWithFetchReference(t *testing.T) {
+	o, r := newTestAPIServer()
+	o.On("Authorize", mock.Anything, mock.Anything).Return(nil)
+	req := httptest.NewRequest("GET", "/api/v1/namespaces/mynamespace/events?fetchreference", nil)
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	res := httptest.NewRecorder()
+
+	var ten int64 = 10
+	o.On("GetEventsWithReferences", mock.Anything, mock.Anything).
+		Return([]*core.EnrichedEvent{}, &database.FilterResult{
+			TotalCount: &ten,
+		}, nil)
+	r.ServeHTTP(res, req)
+
+	assert.Equal(t, 200, res.Result().StatusCode)
+	var resWithCount filterResultsWithCount
+	err := json.NewDecoder(res.Body).Decode(&resWithCount)
+	assert.NoError(t, err)
+	assert.NotNil(t, resWithCount.Items)
+	assert.Equal(t, int64(0), resWithCount.Count)
+	assert.Equal(t, int64(10), resWithCount.Total)
+}
diff --git a/internal/orchestrator/data_query_test.go b/internal/orchestrator/data_query_test.go
index a696f678b..b30d807e3 100644
--- a/internal/orchestrator/data_query_test.go
+++ b/internal/orchestrator/data_query_test.go
@@ -691,6 +691,49 @@ func TestGetEventsWithReferencesEnrichFail(t *testing.T) {
 	assert.EqualError(t, err, "pop")
 }
 
+func TestGetEventWithReferenceQueryFail(t *testing.T) {
+	or := newTestOrchestrator()
+	defer or.cleanup(t)
+	id := fftypes.NewUUID()
+	or.mdi.On("GetEventByID", mock.Anything, "ns", mock.Anything).Return(nil, fmt.Errorf("pop"))
+	_, err := or.GetEventByIDWithReference(context.Background(), id.String())
+	assert.EqualError(t, err, "pop")
+}
+
+func TestGetEventWithReferenceParseUUIDFail(t *testing.T) {
+	or := newTestOrchestrator()
+	defer or.cleanup(t)
+	_, err := or.GetEventByIDWithReference(context.Background(), "1")
+	assert.Regexp(t, "FF00138", err)
+}
+
+func TestGetEventWithReferenceOK(t *testing.T) {
+	or := newTestOrchestrator()
+	defer or.cleanup(t)
+
+	// Setup the IDs
+	ref1 := fftypes.NewUUID()
+	ev1 := fftypes.NewUUID()
+
+	blockchainEvent := &core.Event{
+		ID:        ev1,
+		Sequence:  10000001,
+		Reference: ref1,
+		Type:      core.EventTypeBlockchainEventReceived,
+	}
+
+	or.mem.On("EnrichEvent", mock.Anything, blockchainEvent).Return(&core.EnrichedEvent{
+		Event: *blockchainEvent,
+		BlockchainEvent: &core.BlockchainEvent{
+			ID: ref1,
+		},
+	}, nil)
+
+	or.mdi.On("GetEventByID", mock.Anything, "ns", mock.Anything).Return(blockchainEvent, nil)
+	_, err := or.GetEventByIDWithReference(context.Background(), ev1.String())
+	assert.NoError(t, err)
+}
+
 func TestGetBlockchainEventByID(t *testing.T) {
 	or := newTestOrchestrator()
 	defer or.cleanup(t)