From 2d6fa09357485fea8f3710e529c214211529ad73 Mon Sep 17 00:00:00 2001 From: Kaijun Zhang Date: Wed, 1 Apr 2026 13:40:13 -0400 Subject: [PATCH 1/4] -cherrypick: custom-topics Signed-off-by: Kaijun Zhang --- doc-site/docs/swagger/swagger.yaml | 70 ++++++++++ .../route_post_contract_api_publish.go | 3 +- .../route_post_contract_api_publish_test.go | 21 ++- .../route_post_contract_interface_publish.go | 3 +- ...te_post_contract_interface_publish_test.go | 21 ++- .../apiserver/route_post_new_contract_api.go | 3 +- .../route_post_new_contract_api_test.go | 23 +++- .../route_post_new_contract_interface.go | 3 +- .../route_post_new_contract_interface_test.go | 23 +++- internal/apiserver/route_put_contract_api.go | 3 +- .../apiserver/route_put_contract_api_test.go | 23 +++- internal/coremsgs/en_api_translations.go | 2 + internal/definitions/sender.go | 31 +++-- internal/definitions/sender_contracts.go | 24 ++-- internal/definitions/sender_contracts_test.go | 126 +++++++++++++----- internal/definitions/sender_datatype.go | 2 +- internal/definitions/sender_identity.go | 6 +- internal/definitions/sender_test.go | 6 +- internal/definitions/sender_tokenpool.go | 2 +- mocks/definitionsmocks/sender.go | 56 ++++---- 20 files changed, 348 insertions(+), 103 deletions(-) diff --git a/doc-site/docs/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml index d4e4e5ee79..1d7ca1811a 100644 --- a/doc-site/docs/swagger/swagger.yaml +++ b/doc-site/docs/swagger/swagger.yaml @@ -171,6 +171,13 @@ paths: name: publish schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -1800,6 +1807,13 @@ paths: name: confirm schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -2411,6 +2425,13 @@ paths: schema: example: "true" type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -3828,6 +3849,13 @@ paths: name: publish schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -4856,6 +4884,13 @@ paths: name: confirm schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -13721,6 +13756,13 @@ paths: name: publish schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -15610,6 +15652,13 @@ paths: name: confirm schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -16501,6 +16550,13 @@ paths: schema: example: "true" type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -17981,6 +18037,13 @@ paths: name: publish schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -19037,6 +19100,13 @@ paths: name: confirm schema: type: string + - description: Custom topics for ordering definition broadcast messages + in: query + name: topics + schema: + items: + type: string + type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header diff --git a/internal/apiserver/route_post_contract_api_publish.go b/internal/apiserver/route_post_contract_api_publish.go index f9c4602a8b..21de711157 100644 --- a/internal/apiserver/route_post_contract_api_publish.go +++ b/internal/apiserver/route_post_contract_api_publish.go @@ -35,6 +35,7 @@ var postContractAPIPublish = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostContractAPIPublish, JSONInputValue: func() interface{} { return &core.DefinitionPublish{} }, @@ -45,7 +46,7 @@ var postContractAPIPublish = &ffapi.Route{ waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) input := r.Input.(*core.DefinitionPublish) - return cr.or.DefinitionSender().PublishContractAPI(cr.ctx, cr.apiBaseURL, r.PP["apiName"], input.NetworkName, waitConfirm) + return cr.or.DefinitionSender().PublishContractAPI(cr.ctx, cr.apiBaseURL, r.PP["apiName"], input.NetworkName, waitConfirm, r.QAP["topics"]) }, }, } diff --git a/internal/apiserver/route_post_contract_api_publish_test.go b/internal/apiserver/route_post_contract_api_publish_test.go index d40529ac94..3e460e1adf 100644 --- a/internal/apiserver/route_post_contract_api_publish_test.go +++ b/internal/apiserver/route_post_contract_api_publish_test.go @@ -41,7 +41,26 @@ func TestPostContractAPIPublish(t *testing.T) { res := httptest.NewRecorder() api := &core.ContractAPI{} - mds.On("PublishContractAPI", mock.Anything, "http://127.0.0.1:5000/api/v1/namespaces/ns1", "banana", "banana-net", false).Return(api, nil) + mds.On("PublishContractAPI", mock.Anything, "http://127.0.0.1:5000/api/v1/namespaces/ns1", "banana", "banana-net", false, mock.Anything).Return(api, nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} + +func TestPostContractAPIPublishWithTopics(t *testing.T) { + o, r := newTestAPIServer() + o.On("Authorize", mock.Anything, mock.Anything).Return(nil) + mds := &definitionsmocks.Sender{} + o.On("DefinitionSender").Return(mds) + input := core.DefinitionPublish{NetworkName: "banana-net"} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/apis/banana/publish?topics=my-topic", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + api := &core.ContractAPI{} + + mds.On("PublishContractAPI", mock.Anything, "http://127.0.0.1:5000/api/v1/namespaces/ns1", "banana", "banana-net", false, []string{"my-topic"}).Return(api, nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) diff --git a/internal/apiserver/route_post_contract_interface_publish.go b/internal/apiserver/route_post_contract_interface_publish.go index 56790402a4..9c3ecb5096 100644 --- a/internal/apiserver/route_post_contract_interface_publish.go +++ b/internal/apiserver/route_post_contract_interface_publish.go @@ -36,6 +36,7 @@ var postContractInterfacePublish = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostContractInterfacePublish, JSONInputValue: func() interface{} { return &core.DefinitionPublish{} }, @@ -46,7 +47,7 @@ var postContractInterfacePublish = &ffapi.Route{ waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) input := r.Input.(*core.DefinitionPublish) - return cr.or.DefinitionSender().PublishFFI(cr.ctx, r.PP["name"], r.PP["version"], input.NetworkName, waitConfirm) + return cr.or.DefinitionSender().PublishFFI(cr.ctx, r.PP["name"], r.PP["version"], input.NetworkName, waitConfirm, r.QAP["topics"]) }, }, } diff --git a/internal/apiserver/route_post_contract_interface_publish_test.go b/internal/apiserver/route_post_contract_interface_publish_test.go index 16f9279c83..541e9c23ac 100644 --- a/internal/apiserver/route_post_contract_interface_publish_test.go +++ b/internal/apiserver/route_post_contract_interface_publish_test.go @@ -42,7 +42,26 @@ func TestPostContractInterfacePublish(t *testing.T) { res := httptest.NewRecorder() ffi := &fftypes.FFI{} - mds.On("PublishFFI", mock.Anything, "ffi1", "1.0", "", false).Return(ffi, nil) + mds.On("PublishFFI", mock.Anything, "ffi1", "1.0", "", false, mock.Anything).Return(ffi, nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} + +func TestPostContractInterfacePublishWithTopics(t *testing.T) { + o, r := newTestAPIServer() + o.On("Authorize", mock.Anything, mock.Anything).Return(nil) + mds := &definitionsmocks.Sender{} + o.On("DefinitionSender").Return(mds) + input := core.TokenPool{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces/ffi1/1.0/publish?topics=my-topic", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + ffi := &fftypes.FFI{} + + mds.On("PublishFFI", mock.Anything, "ffi1", "1.0", "", false, []string{"my-topic"}).Return(ffi, nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) diff --git a/internal/apiserver/route_post_new_contract_api.go b/internal/apiserver/route_post_new_contract_api.go index e9bfe34879..422a34a2d8 100644 --- a/internal/apiserver/route_post_new_contract_api.go +++ b/internal/apiserver/route_post_new_contract_api.go @@ -34,6 +34,7 @@ var postNewContractAPI = &ffapi.Route{ QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostNewContractAPI, JSONInputValue: func() interface{} { return &core.ContractAPI{} }, @@ -49,7 +50,7 @@ var postNewContractAPI = &ffapi.Route{ api := r.Input.(*core.ContractAPI) api.ID = nil api.Published = strings.EqualFold(r.QP["publish"], "true") - err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm) + err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, r.QAP["topics"]) return api, err }, }, diff --git a/internal/apiserver/route_post_new_contract_api_test.go b/internal/apiserver/route_post_new_contract_api_test.go index 97c668e6cd..05d138ac42 100644 --- a/internal/apiserver/route_post_new_contract_api_test.go +++ b/internal/apiserver/route_post_new_contract_api_test.go @@ -42,7 +42,7 @@ func TestPostNewContractAPI(t *testing.T) { req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), false).Return(nil) + mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), false, mock.Anything).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) @@ -61,8 +61,27 @@ func TestPostNewContractAPISync(t *testing.T) { req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), true).Return(nil) + mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), true, mock.Anything).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 200, res.Result().StatusCode) } + +func TestPostNewContractAPIWithTopics(t *testing.T) { + o, r := newTestAPIServer() + o.On("Authorize", mock.Anything, mock.Anything).Return(nil) + mds := &definitionsmocks.Sender{} + o.On("Contracts").Return(&contractmocks.Manager{}) + o.On("DefinitionSender").Return(mds) + input := core.Datatype{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/apis?topics=my-topic", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), false, []string{"my-topic"}).Return(nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} diff --git a/internal/apiserver/route_post_new_contract_interface.go b/internal/apiserver/route_post_new_contract_interface.go index abfd08acea..848177962a 100644 --- a/internal/apiserver/route_post_new_contract_interface.go +++ b/internal/apiserver/route_post_new_contract_interface.go @@ -34,6 +34,7 @@ var postNewContractInterface = &ffapi.Route{ QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostNewContractInterface, JSONInputValue: func() interface{} { return &fftypes.FFI{} }, @@ -48,7 +49,7 @@ var postNewContractInterface = &ffapi.Route{ r.SuccessStatus = syncRetcode(waitConfirm) ffi := r.Input.(*fftypes.FFI) ffi.Published = strings.EqualFold(r.QP["publish"], "true") - err = cr.or.DefinitionSender().DefineFFI(cr.ctx, ffi, waitConfirm) + err = cr.or.DefinitionSender().DefineFFI(cr.ctx, ffi, waitConfirm, r.QAP["topics"]) return ffi, err }, }, diff --git a/internal/apiserver/route_post_new_contract_interface_test.go b/internal/apiserver/route_post_new_contract_interface_test.go index 34b4794600..777880c094 100644 --- a/internal/apiserver/route_post_new_contract_interface_test.go +++ b/internal/apiserver/route_post_new_contract_interface_test.go @@ -42,7 +42,7 @@ func TestPostNewContractInterface(t *testing.T) { req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - mds.On("DefineFFI", mock.Anything, mock.AnythingOfType("*fftypes.FFI"), false).Return(nil) + mds.On("DefineFFI", mock.Anything, mock.AnythingOfType("*fftypes.FFI"), false, mock.Anything).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) @@ -61,8 +61,27 @@ func TestPostNewContractInterfaceSync(t *testing.T) { req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - mds.On("DefineFFI", mock.Anything, mock.AnythingOfType("*fftypes.FFI"), true).Return(nil) + mds.On("DefineFFI", mock.Anything, mock.AnythingOfType("*fftypes.FFI"), true, mock.Anything).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 200, res.Result().StatusCode) } + +func TestPostNewContractInterfaceWithTopics(t *testing.T) { + o, r := newTestAPIServer() + o.On("Authorize", mock.Anything, mock.Anything).Return(nil) + mds := &definitionsmocks.Sender{} + o.On("Contracts").Return(&contractmocks.Manager{}) + o.On("DefinitionSender").Return(mds) + input := core.Datatype{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces?topics=my-topic", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + mds.On("DefineFFI", mock.Anything, mock.AnythingOfType("*fftypes.FFI"), false, []string{"my-topic"}).Return(nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} diff --git a/internal/apiserver/route_put_contract_api.go b/internal/apiserver/route_put_contract_api.go index 7cbd0b5f72..0e8f90c3ec 100644 --- a/internal/apiserver/route_put_contract_api.go +++ b/internal/apiserver/route_put_contract_api.go @@ -36,6 +36,7 @@ var putContractAPI = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, + {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, }, Description: coremsgs.APIParamsContractAPIID, JSONInputValue: func() interface{} { return &core.ContractAPI{} }, @@ -51,7 +52,7 @@ var putContractAPI = &ffapi.Route{ api := r.Input.(*core.ContractAPI) api.ID, err = fftypes.ParseUUID(cr.ctx, r.PP["id"]) if err == nil { - err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm) + err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, r.QAP["topics"]) } return api, err }, diff --git a/internal/apiserver/route_put_contract_api_test.go b/internal/apiserver/route_put_contract_api_test.go index eb6369c03c..3e4801d6b2 100644 --- a/internal/apiserver/route_put_contract_api_test.go +++ b/internal/apiserver/route_put_contract_api_test.go @@ -42,7 +42,7 @@ func TestPutContractAPI(t *testing.T) { req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), false).Return(nil) + mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), false, mock.Anything).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) @@ -61,8 +61,27 @@ func TestPutContractAPISync(t *testing.T) { req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), true).Return(nil) + mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), true, mock.Anything).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 200, res.Result().StatusCode) } + +func TestPutContractAPIWithTopics(t *testing.T) { + o, r := newTestAPIServer() + o.On("Authorize", mock.Anything, mock.Anything).Return(nil) + mds := &definitionsmocks.Sender{} + o.On("DefinitionSender").Return(mds) + o.On("Contracts").Return(&contractmocks.Manager{}) + input := core.Datatype{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("PUT", "/api/v1/namespaces/ns1/apis/99EEE458-037C-4C78-B66B-31E52F93D2E9?topics=my-topic", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + mds.On("DefineContractAPI", mock.Anything, mock.Anything, mock.AnythingOfType("*core.ContractAPI"), false, []string{"my-topic"}).Return(nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index 02f1a96d5c..4d3c5d84a7 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -216,4 +216,6 @@ var ( APISubscriptionStartSequenceID = ffm("api.startsequenceid", "The sequence ID in the raw event stream to start indexing through events from. Leave blank to start indexing from the most recent events") APISubscriptionEndSequenceID = ffm("api.endsequenceid", "The sequence ID in the raw event stream to stop indexing through events at. Leave blank to start indexing from the most recent events") + + APICustomTopicsQueryParam = ffm("api.customTopicsQueryParam", "Custom topics for ordering definition broadcast messages") ) diff --git a/internal/definitions/sender.go b/internal/definitions/sender.go index 70f3664095..5e3f401fb1 100644 --- a/internal/definitions/sender.go +++ b/internal/definitions/sender.go @@ -43,10 +43,10 @@ type Sender interface { DefineDatatype(ctx context.Context, datatype *core.Datatype, waitConfirm bool) error DefineTokenPool(ctx context.Context, pool *core.TokenPool, waitConfirm bool) error PublishTokenPool(ctx context.Context, poolNameOrID, networkName string, waitConfirm bool) (*core.TokenPool, error) - DefineFFI(ctx context.Context, ffi *fftypes.FFI, waitConfirm bool) error - PublishFFI(ctx context.Context, name, version, networkName string, waitConfirm bool) (*fftypes.FFI, error) - DefineContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI, waitConfirm bool) error - PublishContractAPI(ctx context.Context, httpServerURL, name, networkName string, waitConfirm bool) (api *core.ContractAPI, err error) + DefineFFI(ctx context.Context, ffi *fftypes.FFI, waitConfirm bool, topics []string) error + PublishFFI(ctx context.Context, name, version, networkName string, waitConfirm bool, topics []string) (*fftypes.FFI, error) + DefineContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI, waitConfirm bool, topics []string) error + PublishContractAPI(ctx context.Context, httpServerURL, name, networkName string, waitConfirm bool, topics []string) (api *core.ContractAPI, err error) } type definitionSender struct { @@ -135,35 +135,46 @@ func (w *sendWrapper) send(ctx context.Context, waitConfirm bool) (*core.Message } } -func (ds *definitionSender) getSenderDefault(ctx context.Context, def core.Definition, tag string) *sendWrapper { +func (ds *definitionSender) getSenderDefault(ctx context.Context, def core.Definition, tag string, customTopics []string) *sendWrapper { org, err := ds.identity.GetRootOrg(ctx) if err != nil { return wrapSendError(err) } return ds.getSender(ctx, def, &core.SignerRef{ /* resolve to node default */ Author: org.DID, - }, tag) + }, tag, customTopics) } -func (ds *definitionSender) getSender(ctx context.Context, def core.Definition, signingIdentity *core.SignerRef, tag string) *sendWrapper { +func (ds *definitionSender) getSender(ctx context.Context, def core.Definition, signingIdentity *core.SignerRef, tag string, customTopics []string) *sendWrapper { err := ds.identity.ResolveInputSigningIdentity(ctx, signingIdentity) if err != nil { return wrapSendError(err) } - return ds.getSenderResolved(ctx, def, signingIdentity, tag) + return ds.getSenderResolved(ctx, def, signingIdentity, tag, customTopics) } -func (ds *definitionSender) getSenderResolved(ctx context.Context, def core.Definition, signingIdentity *core.SignerRef, tag string) *sendWrapper { +func (ds *definitionSender) getSenderResolved(ctx context.Context, def core.Definition, signingIdentity *core.SignerRef, tag string, customTopics []string) *sendWrapper { b, err := json.Marshal(&def) if err != nil { return wrapSendError(i18n.WrapError(ctx, err, coremsgs.MsgSerializationFailed)) } + topics := fftypes.FFStringArray{def.Topic()} + + var filtered []string + for _, t := range customTopics { + if t != "" { + filtered = append(filtered, t) + } + } + if len(filtered) > 0 { + topics = fftypes.FFStringArray(filtered) + } dataValue := fftypes.JSONAnyPtrBytes(b) message := &core.MessageInOut{ Message: core.Message{ Header: core.MessageHeader{ Type: core.MessageTypeDefinition, - Topics: fftypes.FFStringArray{def.Topic()}, + Topics: topics, Tag: tag, TxType: core.TransactionTypeBatchPin, SignerRef: *signingIdentity, diff --git a/internal/definitions/sender_contracts.go b/internal/definitions/sender_contracts.go index 7264a9f926..6c83a9562a 100644 --- a/internal/definitions/sender_contracts.go +++ b/internal/definitions/sender_contracts.go @@ -26,7 +26,7 @@ import ( "github.com/hyperledger/firefly/pkg/core" ) -func (ds *definitionSender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, waitConfirm bool) error { +func (ds *definitionSender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, waitConfirm bool, topics []string) error { ffi.ID = fftypes.NewUUID() ffi.Namespace = ds.namespace for _, method := range ffi.Methods { @@ -43,7 +43,7 @@ func (ds *definitionSender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, wai if !ds.multiparty { return i18n.NewError(ctx, coremsgs.MsgActionNotSupported) } - _, err := ds.getFFISender(ctx, ffi).send(ctx, waitConfirm) + _, err := ds.getFFISender(ctx, ffi, topics).send(ctx, waitConfirm) return err } @@ -60,7 +60,7 @@ func (ds *definitionSender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, wai }) } -func (ds *definitionSender) getFFISender(ctx context.Context, ffi *fftypes.FFI) *sendWrapper { +func (ds *definitionSender) getFFISender(ctx context.Context, ffi *fftypes.FFI, topics []string) *sendWrapper { if err := ds.contracts.ResolveFFI(ctx, ffi); err != nil { return wrapSendError(err) } @@ -82,7 +82,7 @@ func (ds *definitionSender) getFFISender(ctx context.Context, ffi *fftypes.FFI) ffi.Namespace = "" ffi.Published = true - sender := ds.getSenderDefault(ctx, ffi, core.SystemTagDefineFFI) + sender := ds.getSenderDefault(ctx, ffi, core.SystemTagDefineFFI, topics) if sender.message != nil { ffi.Message = sender.message.Header.ID } @@ -92,7 +92,7 @@ func (ds *definitionSender) getFFISender(ctx context.Context, ffi *fftypes.FFI) return sender } -func (ds *definitionSender) PublishFFI(ctx context.Context, name, version, networkName string, waitConfirm bool) (ffi *fftypes.FFI, err error) { +func (ds *definitionSender) PublishFFI(ctx context.Context, name, version, networkName string, waitConfirm bool, topics []string) (ffi *fftypes.FFI, err error) { if !ds.multiparty { return nil, i18n.NewError(ctx, coremsgs.MsgActionNotSupported) } @@ -106,7 +106,7 @@ func (ds *definitionSender) PublishFFI(ctx context.Context, name, version, netwo return i18n.NewError(ctx, coremsgs.MsgAlreadyPublished) } ffi.NetworkName = networkName - sender = ds.getFFISender(ctx, ffi) + sender = ds.getFFISender(ctx, ffi, topics) if sender.err != nil { return sender.err } @@ -120,7 +120,7 @@ func (ds *definitionSender) PublishFFI(ctx context.Context, name, version, netwo return ffi, err } -func (ds *definitionSender) DefineContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI, waitConfirm bool) error { +func (ds *definitionSender) DefineContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI, waitConfirm bool, topics []string) error { if api.ID == nil { api.ID = fftypes.NewUUID() } @@ -130,7 +130,7 @@ func (ds *definitionSender) DefineContractAPI(ctx context.Context, httpServerURL if !ds.multiparty { return i18n.NewError(ctx, coremsgs.MsgActionNotSupported) } - _, err := ds.getContractAPISender(ctx, httpServerURL, api).send(ctx, waitConfirm) + _, err := ds.getContractAPISender(ctx, httpServerURL, api, topics).send(ctx, waitConfirm) return err } @@ -141,7 +141,7 @@ func (ds *definitionSender) DefineContractAPI(ctx context.Context, httpServerURL }) } -func (ds *definitionSender) getContractAPISender(ctx context.Context, httpServerURL string, api *core.ContractAPI) *sendWrapper { +func (ds *definitionSender) getContractAPISender(ctx context.Context, httpServerURL string, api *core.ContractAPI, topics []string) *sendWrapper { if err := ds.contracts.ResolveContractAPI(ctx, httpServerURL, api); err != nil { return wrapSendError(err) } @@ -174,7 +174,7 @@ func (ds *definitionSender) getContractAPISender(ctx context.Context, httpServer api.Namespace = "" api.Published = true - sender := ds.getSenderDefault(ctx, api, core.SystemTagDefineContractAPI) + sender := ds.getSenderDefault(ctx, api, core.SystemTagDefineContractAPI, topics) if sender.message != nil { api.Message = sender.message.Header.ID } @@ -184,7 +184,7 @@ func (ds *definitionSender) getContractAPISender(ctx context.Context, httpServer return sender } -func (ds *definitionSender) PublishContractAPI(ctx context.Context, httpServerURL, name, networkName string, waitConfirm bool) (api *core.ContractAPI, err error) { +func (ds *definitionSender) PublishContractAPI(ctx context.Context, httpServerURL, name, networkName string, waitConfirm bool, topics []string) (api *core.ContractAPI, err error) { if !ds.multiparty { return nil, i18n.NewError(ctx, coremsgs.MsgActionNotSupported) } @@ -198,7 +198,7 @@ func (ds *definitionSender) PublishContractAPI(ctx context.Context, httpServerUR return i18n.NewError(ctx, coremsgs.MsgAlreadyPublished) } api.NetworkName = networkName - sender = ds.getContractAPISender(ctx, httpServerURL, api) + sender = ds.getContractAPISender(ctx, httpServerURL, api, topics) if sender.err != nil { return sender.err } diff --git a/internal/definitions/sender_contracts_test.go b/internal/definitions/sender_contracts_test.go index b0717cc460..9f71de6b56 100644 --- a/internal/definitions/sender_contracts_test.go +++ b/internal/definitions/sender_contracts_test.go @@ -44,7 +44,7 @@ func TestDefineFFIResolveFail(t *testing.T) { ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(fmt.Errorf("pop")) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.EqualError(t, err, "pop") } @@ -63,7 +63,7 @@ func TestDefineFFIFail(t *testing.T) { ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(nil) ds.mim.On("GetRootOrg", context.Background()).Return(nil, fmt.Errorf("pop")) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.EqualError(t, err, "pop") } @@ -80,7 +80,7 @@ func TestDefineFFIFailInnerError(t *testing.T) { ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(nil) ds.mdi.On("InsertOrGetFFI", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("error2: [%w]", fmt.Errorf("pop"))) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.Regexp(t, "pop", err) } @@ -98,7 +98,7 @@ func TestDefineFFIExists(t *testing.T) { ds.mdi.On("GetFFIByNetworkName", context.Background(), "ns1", "ffi1", "1.0").Return(&fftypes.FFI{}, nil) ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(nil) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.Regexp(t, "FF10448", err) } @@ -116,7 +116,7 @@ func TestDefineFFIQueryFail(t *testing.T) { ds.mdi.On("GetFFIByNetworkName", context.Background(), "ns1", "ffi1", "1.0").Return(nil, fmt.Errorf("pop")) ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(nil) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.EqualError(t, err, "pop") } @@ -144,7 +144,7 @@ func TestDefineFFIOk(t *testing.T) { ds.mbm.On("NewBroadcast", mock.Anything).Return(mms) mms.On("Send", context.Background()).Return(nil) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.NoError(t, err) mms.AssertExpectations(t) @@ -174,7 +174,7 @@ func TestDefineFFIConfirm(t *testing.T) { ds.mbm.On("NewBroadcast", mock.Anything).Return(mms) mms.On("SendAndWait", context.Background()).Return(nil) - err := ds.DefineFFI(context.Background(), ffi, true) + err := ds.DefineFFI(context.Background(), ffi, true, nil) assert.NoError(t, err) mms.AssertExpectations(t) @@ -191,7 +191,7 @@ func TestDefineFFIPublishNonMultiparty(t *testing.T) { Published: true, } - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.Regexp(t, "FF10414", err) } @@ -208,7 +208,7 @@ func TestDefineFFINonMultiparty(t *testing.T) { ds.mdi.On("InsertOrGetFFI", context.Background(), ffi).Return(nil, nil) ds.mdi.On("InsertEvent", context.Background(), mock.Anything).Return(nil) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.NoError(t, err) } @@ -223,10 +223,41 @@ func TestDefineFFINonMultipartyFail(t *testing.T) { ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(fmt.Errorf("pop")) - err := ds.DefineFFI(context.Background(), ffi, false) + err := ds.DefineFFI(context.Background(), ffi, false, nil) assert.Regexp(t, "FF10403", err) } +// test that we can call defineFFI with custom topics +func TestDefineFFICustomTopics(t *testing.T) { + ds := newTestDefinitionSender(t) + defer ds.cleanup(t) + ds.multiparty = true + + ffi := &fftypes.FFI{ + Name: "ffi1", + Version: "1.0", + Published: true, + } + + ds.mdi.On("GetFFIByNetworkName", context.Background(), "ns1", "ffi1", "1.0").Return(nil, nil) + ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(nil) + ds.mim.On("GetRootOrg", context.Background()).Return(&core.Identity{ + IdentityBase: core.IdentityBase{ + DID: "firefly:org1", + }, + }, nil) + ds.mim.On("ResolveInputSigningIdentity", context.Background(), mock.Anything).Return(nil) + + mms := &syncasyncmocks.Sender{} + ds.mbm.On("NewBroadcast", mock.Anything).Return(mms) + mms.On("Send", context.Background()).Return(nil) + + err := ds.DefineFFI(context.Background(), ffi, false, []string{"my-custom-topic"}) + assert.NoError(t, err) + + mms.AssertExpectations(t) +} + func TestDefineContractAPIResolveFail(t *testing.T) { ds := newTestDefinitionSender(t) defer ds.cleanup(t) @@ -240,7 +271,7 @@ func TestDefineContractAPIResolveFail(t *testing.T) { ds.mcm.On("ResolveContractAPI", context.Background(), url, api).Return(fmt.Errorf("pop")) - err := ds.DefineContractAPI(context.Background(), url, api, false) + err := ds.DefineContractAPI(context.Background(), url, api, false, nil) assert.EqualError(t, err, "pop") } @@ -259,7 +290,7 @@ func TestDefineContractAPIFail(t *testing.T) { ds.mim.On("GetRootOrg", context.Background()).Return(nil, fmt.Errorf("pop")) ds.mdi.On("GetContractAPIByNetworkName", context.Background(), "ns1", "banana").Return(nil, nil) - err := ds.DefineContractAPI(context.Background(), url, api, false) + err := ds.DefineContractAPI(context.Background(), url, api, false, nil) assert.EqualError(t, err, "pop") } @@ -287,7 +318,7 @@ func TestDefineContractAPIOk(t *testing.T) { ds.mbm.On("NewBroadcast", mock.Anything).Return(mms) mms.On("Send", context.Background()).Return(nil) - err := ds.DefineContractAPI(context.Background(), url, api, false) + err := ds.DefineContractAPI(context.Background(), url, api, false, nil) assert.NoError(t, err) mms.AssertExpectations(t) @@ -304,7 +335,7 @@ func TestDefineContractAPINonMultiparty(t *testing.T) { ds.mdi.On("InsertOrGetContractAPI", mock.Anything, mock.Anything).Return(nil, nil) ds.mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil) - err := ds.DefineContractAPI(context.Background(), url, api, false) + err := ds.DefineContractAPI(context.Background(), url, api, false, nil) assert.NoError(t, err) } @@ -319,7 +350,7 @@ func TestDefineContractAPIPublishNonMultiparty(t *testing.T) { Published: true, } - err := ds.DefineContractAPI(context.Background(), url, api, false) + err := ds.DefineContractAPI(context.Background(), url, api, false, nil) assert.Regexp(t, "FF10414", err) } @@ -340,10 +371,41 @@ func TestDefineContractAPINonMultipartyUpdate(t *testing.T) { ds.mdi.On("UpsertContractAPI", mock.Anything, mock.Anything, mock.Anything).Return(nil) ds.mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil) - err := ds.DefineContractAPI(context.Background(), url, api, false) + err := ds.DefineContractAPI(context.Background(), url, api, false, nil) assert.NoError(t, err) } +// test that we can call defineContractAPI with custom topics +func TestDefineContractAPICustomTopics(t *testing.T) { + ds := newTestDefinitionSender(t) + defer ds.cleanup(t) + ds.multiparty = true + + url := "http://firefly" + api := &core.ContractAPI{ + Name: "banana", + Published: true, + } + + ds.mcm.On("ResolveContractAPI", context.Background(), url, api).Return(nil) + ds.mim.On("GetRootOrg", context.Background()).Return(&core.Identity{ + IdentityBase: core.IdentityBase{ + DID: "firefly:org1", + }, + }, nil) + ds.mim.On("ResolveInputSigningIdentity", context.Background(), mock.Anything).Return(nil) + ds.mdi.On("GetContractAPIByNetworkName", context.Background(), "ns1", "banana").Return(nil, nil) + + mms := &syncasyncmocks.Sender{} + ds.mbm.On("NewBroadcast", mock.Anything).Return(mms) + mms.On("Send", context.Background()).Return(nil) + + err := ds.DefineContractAPI(context.Background(), url, api, false, []string{"my-custom-topic"}) + assert.NoError(t, err) + + mms.AssertExpectations(t) +} + func TestPublishFFI(t *testing.T) { ds := newTestDefinitionSender(t) defer ds.cleanup(t) @@ -372,7 +434,7 @@ func TestPublishFFI(t *testing.T) { mms.On("Send", context.Background()).Return(nil) mockRunAsGroupPassthrough(ds.mdi) - result, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false) + result, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false, nil) assert.NoError(t, err) assert.Equal(t, ffi, result) assert.True(t, ffi.Published) @@ -395,7 +457,7 @@ func TestPublishFFIAlreadyPublished(t *testing.T) { ds.mcm.On("GetFFIWithChildren", context.Background(), "ffi1", "1.0").Return(ffi, nil) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false) + _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false, nil) assert.Regexp(t, "FF10450", err) } @@ -407,7 +469,7 @@ func TestPublishFFIQueryFail(t *testing.T) { ds.mcm.On("GetFFIWithChildren", context.Background(), "ffi1", "1.0").Return(nil, fmt.Errorf("pop")) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false) + _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false, nil) assert.EqualError(t, err, "pop") } @@ -427,7 +489,7 @@ func TestPublishFFIResolveFail(t *testing.T) { ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(fmt.Errorf("pop")) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false) + _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false, nil) assert.EqualError(t, err, "pop") } @@ -458,7 +520,7 @@ func TestPublishFFIPrepareFail(t *testing.T) { mms.On("Prepare", context.Background()).Return(fmt.Errorf("pop")) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false) + _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false, nil) assert.EqualError(t, err, "pop") mms.AssertExpectations(t) @@ -469,7 +531,7 @@ func TestPublishFFINonMultiparty(t *testing.T) { defer ds.cleanup(t) ds.multiparty = false - _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false) + _, err := ds.PublishFFI(context.Background(), "ffi1", "1.0", "ffi1-shared", false, nil) assert.Regexp(t, "FF10414", err) } @@ -501,7 +563,7 @@ func TestPublishContractAPI(t *testing.T) { mms.On("Send", context.Background()).Return(nil) mockRunAsGroupPassthrough(ds.mdi) - result, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + result, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.NoError(t, err) assert.Equal(t, api, result) assert.True(t, api.Published) @@ -524,7 +586,7 @@ func TestPublishContractAPIAlreadyPublished(t *testing.T) { ds.mcm.On("GetContractAPI", context.Background(), url, "api").Return(api, nil) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.Regexp(t, "FF10450", err) } @@ -538,7 +600,7 @@ func TestPublishContractAPIQueryFail(t *testing.T) { ds.mcm.On("GetContractAPI", context.Background(), url, "api").Return(nil, fmt.Errorf("pop")) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.EqualError(t, err, "pop") } @@ -558,7 +620,7 @@ func TestPublishContractAPIResolveFail(t *testing.T) { ds.mcm.On("ResolveContractAPI", context.Background(), url, api).Return(fmt.Errorf("pop")) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.EqualError(t, err, "pop") } @@ -569,7 +631,7 @@ func TestPublishContractAPINonMultiparty(t *testing.T) { url := "http://firefly" - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.Regexp(t, "FF10414", err) } @@ -590,7 +652,7 @@ func TestPublishContractAPINetworkNameFail(t *testing.T) { ds.mcm.On("ResolveContractAPI", context.Background(), url, api).Return(nil) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.EqualError(t, err, "pop") } @@ -611,7 +673,7 @@ func TestPublishContractAPINetworkNameConflict(t *testing.T) { ds.mcm.On("ResolveContractAPI", context.Background(), url, api).Return(nil) mockRunAsGroupPassthrough(ds.mdi) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.Regexp(t, "FF10448", err) } @@ -636,7 +698,7 @@ func TestPublishContractAPIInterfaceFail(t *testing.T) { mockRunAsGroupPassthrough(ds.mdi) ds.mdi.On("GetFFIByID", context.Background(), "ns1", api.Interface.ID).Return(nil, fmt.Errorf("pop")) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.EqualError(t, err, "pop") } @@ -661,7 +723,7 @@ func TestPublishContractAPIInterfaceNotFound(t *testing.T) { mockRunAsGroupPassthrough(ds.mdi) ds.mdi.On("GetFFIByID", context.Background(), "ns1", api.Interface.ID).Return(nil, nil) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.Regexp(t, "FF10303", err) } @@ -688,6 +750,6 @@ func TestPublishContractAPIInterfaceNotPublished(t *testing.T) { Published: false, }, nil) - _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false) + _, err := ds.PublishContractAPI(context.Background(), url, "api", "api-shared", false, nil) assert.Regexp(t, "FF10451", err) } diff --git a/internal/definitions/sender_datatype.go b/internal/definitions/sender_datatype.go index d34ee9e9e9..6ee3fb025e 100644 --- a/internal/definitions/sender_datatype.go +++ b/internal/definitions/sender_datatype.go @@ -44,7 +44,7 @@ func (ds *definitionSender) DefineDatatype(ctx context.Context, datatype *core.D } datatype.Namespace = "" - msg, err := ds.getSenderDefault(ctx, datatype, core.SystemTagDefineDatatype).send(ctx, waitConfirm) + msg, err := ds.getSenderDefault(ctx, datatype, core.SystemTagDefineDatatype, nil).send(ctx, waitConfirm) if msg != nil { datatype.Message = msg.Header.ID } diff --git a/internal/definitions/sender_identity.go b/internal/definitions/sender_identity.go index 02986684db..3a3db16d7b 100644 --- a/internal/definitions/sender_identity.go +++ b/internal/definitions/sender_identity.go @@ -34,7 +34,7 @@ func (ds *definitionSender) ClaimIdentity(ctx context.Context, claim *core.Ident } claim.Identity.Namespace = "" - claimMsg, err := ds.getSenderResolved(ctx, claim, signingIdentity, core.SystemTagIdentityClaim).send(ctx, false) + claimMsg, err := ds.getSenderResolved(ctx, claim, signingIdentity, core.SystemTagIdentityClaim, nil).send(ctx, false) if err != nil { return err } @@ -48,7 +48,7 @@ func (ds *definitionSender) ClaimIdentity(ctx context.Context, claim *core.Ident Hash: claimMsg.Hash, }, Identity: claim.Identity.IdentityBase, - }, parentSigner, core.SystemTagIdentityVerification).send(ctx, false) + }, parentSigner, core.SystemTagIdentityVerification, nil).send(ctx, false) if err != nil { return err } @@ -66,7 +66,7 @@ func (ds *definitionSender) ClaimIdentity(ctx context.Context, claim *core.Ident func (ds *definitionSender) UpdateIdentity(ctx context.Context, identity *core.Identity, def *core.IdentityUpdate, signingIdentity *core.SignerRef, waitConfirm bool) error { if ds.multiparty { - updateMsg, err := ds.getSender(ctx, def, signingIdentity, core.SystemTagIdentityUpdate).send(ctx, waitConfirm) + updateMsg, err := ds.getSender(ctx, def, signingIdentity, core.SystemTagIdentityUpdate, nil).send(ctx, waitConfirm) identity.Messages.Update = updateMsg.Header.ID return err } diff --git a/internal/definitions/sender_test.go b/internal/definitions/sender_test.go index 261c5c3579..935a845296 100644 --- a/internal/definitions/sender_test.go +++ b/internal/definitions/sender_test.go @@ -149,7 +149,7 @@ func TestCreateDefinitionConfirm(t *testing.T) { mms.On("SendAndWait", mock.Anything).Return(nil) ds.multiparty = true - _, err := ds.getSenderDefault(ds.ctx, &core.Datatype{}, core.SystemTagDefineDatatype).send(ds.ctx, true) + _, err := ds.getSenderDefault(ds.ctx, &core.Datatype{}, core.SystemTagDefineDatatype, nil).send(ds.ctx, true) assert.NoError(t, err) mim.AssertExpectations(t) @@ -176,7 +176,7 @@ func TestCreateDatatypeDefinitionAsNodeConfirm(t *testing.T) { ds.multiparty = true - _, err := ds.getSenderDefault(ds.ctx, &core.Datatype{}, core.SystemTagDefineDatatype).send(ds.ctx, true) + _, err := ds.getSenderDefault(ds.ctx, &core.Datatype{}, core.SystemTagDefineDatatype, nil).send(ds.ctx, true) assert.NoError(t, err) mim.AssertExpectations(t) @@ -195,6 +195,6 @@ func TestCreateDefinitionBadIdentity(t *testing.T) { _, err := ds.getSender(ds.ctx, &core.Datatype{}, &core.SignerRef{ Author: "wrong", Key: "wrong", - }, core.SystemTagDefineDatatype).send(ds.ctx, false) + }, core.SystemTagDefineDatatype, nil).send(ds.ctx, false) assert.Regexp(t, "pop", err) } diff --git a/internal/definitions/sender_tokenpool.go b/internal/definitions/sender_tokenpool.go index 1b02783c51..c7960dc613 100644 --- a/internal/definitions/sender_tokenpool.go +++ b/internal/definitions/sender_tokenpool.go @@ -97,7 +97,7 @@ func (ds *definitionSender) getTokenPoolSender(ctx context.Context, pool *core.T pool.Active = false definition := &core.TokenPoolDefinition{Pool: pool} - sender := ds.getSenderDefault(ctx, definition, core.SystemTagDefinePool) + sender := ds.getSenderDefault(ctx, definition, core.SystemTagDefinePool, nil) if sender.message != nil { pool.Message = sender.message.Header.ID } diff --git a/mocks/definitionsmocks/sender.go b/mocks/definitionsmocks/sender.go index d4720e15c7..374761353e 100644 --- a/mocks/definitionsmocks/sender.go +++ b/mocks/definitionsmocks/sender.go @@ -35,17 +35,17 @@ func (_m *Sender) ClaimIdentity(ctx context.Context, def *core.IdentityClaim, si return r0 } -// DefineContractAPI provides a mock function with given fields: ctx, httpServerURL, api, waitConfirm -func (_m *Sender) DefineContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI, waitConfirm bool) error { - ret := _m.Called(ctx, httpServerURL, api, waitConfirm) +// DefineContractAPI provides a mock function with given fields: ctx, httpServerURL, api, waitConfirm, topics +func (_m *Sender) DefineContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI, waitConfirm bool, topics []string) error { + ret := _m.Called(ctx, httpServerURL, api, waitConfirm, topics) if len(ret) == 0 { panic("no return value specified for DefineContractAPI") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, *core.ContractAPI, bool) error); ok { - r0 = rf(ctx, httpServerURL, api, waitConfirm) + if rf, ok := ret.Get(0).(func(context.Context, string, *core.ContractAPI, bool, []string) error); ok { + r0 = rf(ctx, httpServerURL, api, waitConfirm, topics) } else { r0 = ret.Error(0) } @@ -71,17 +71,17 @@ func (_m *Sender) DefineDatatype(ctx context.Context, datatype *core.Datatype, w return r0 } -// DefineFFI provides a mock function with given fields: ctx, ffi, waitConfirm -func (_m *Sender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, waitConfirm bool) error { - ret := _m.Called(ctx, ffi, waitConfirm) +// DefineFFI provides a mock function with given fields: ctx, ffi, waitConfirm, topics +func (_m *Sender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, waitConfirm bool, topics []string) error { + ret := _m.Called(ctx, ffi, waitConfirm, topics) if len(ret) == 0 { panic("no return value specified for DefineFFI") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFI, bool) error); ok { - r0 = rf(ctx, ffi, waitConfirm) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFI, bool, []string) error); ok { + r0 = rf(ctx, ffi, waitConfirm, topics) } else { r0 = ret.Error(0) } @@ -125,9 +125,9 @@ func (_m *Sender) Name() string { return r0 } -// PublishContractAPI provides a mock function with given fields: ctx, httpServerURL, name, networkName, waitConfirm -func (_m *Sender) PublishContractAPI(ctx context.Context, httpServerURL string, name string, networkName string, waitConfirm bool) (*core.ContractAPI, error) { - ret := _m.Called(ctx, httpServerURL, name, networkName, waitConfirm) +// PublishContractAPI provides a mock function with given fields: ctx, httpServerURL, name, networkName, waitConfirm, topics +func (_m *Sender) PublishContractAPI(ctx context.Context, httpServerURL string, name string, networkName string, waitConfirm bool, topics []string) (*core.ContractAPI, error) { + ret := _m.Called(ctx, httpServerURL, name, networkName, waitConfirm, topics) if len(ret) == 0 { panic("no return value specified for PublishContractAPI") @@ -135,19 +135,19 @@ func (_m *Sender) PublishContractAPI(ctx context.Context, httpServerURL string, var r0 *core.ContractAPI var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) (*core.ContractAPI, error)); ok { - return rf(ctx, httpServerURL, name, networkName, waitConfirm) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool, []string) (*core.ContractAPI, error)); ok { + return rf(ctx, httpServerURL, name, networkName, waitConfirm, topics) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) *core.ContractAPI); ok { - r0 = rf(ctx, httpServerURL, name, networkName, waitConfirm) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool, []string) *core.ContractAPI); ok { + r0 = rf(ctx, httpServerURL, name, networkName, waitConfirm, topics) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*core.ContractAPI) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool) error); ok { - r1 = rf(ctx, httpServerURL, name, networkName, waitConfirm) + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool, []string) error); ok { + r1 = rf(ctx, httpServerURL, name, networkName, waitConfirm, topics) } else { r1 = ret.Error(1) } @@ -155,9 +155,9 @@ func (_m *Sender) PublishContractAPI(ctx context.Context, httpServerURL string, return r0, r1 } -// PublishFFI provides a mock function with given fields: ctx, name, version, networkName, waitConfirm -func (_m *Sender) PublishFFI(ctx context.Context, name string, version string, networkName string, waitConfirm bool) (*fftypes.FFI, error) { - ret := _m.Called(ctx, name, version, networkName, waitConfirm) +// PublishFFI provides a mock function with given fields: ctx, name, version, networkName, waitConfirm, topics +func (_m *Sender) PublishFFI(ctx context.Context, name string, version string, networkName string, waitConfirm bool, topics []string) (*fftypes.FFI, error) { + ret := _m.Called(ctx, name, version, networkName, waitConfirm, topics) if len(ret) == 0 { panic("no return value specified for PublishFFI") @@ -165,19 +165,19 @@ func (_m *Sender) PublishFFI(ctx context.Context, name string, version string, n var r0 *fftypes.FFI var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) (*fftypes.FFI, error)); ok { - return rf(ctx, name, version, networkName, waitConfirm) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool, []string) (*fftypes.FFI, error)); ok { + return rf(ctx, name, version, networkName, waitConfirm, topics) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) *fftypes.FFI); ok { - r0 = rf(ctx, name, version, networkName, waitConfirm) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool, []string) *fftypes.FFI); ok { + r0 = rf(ctx, name, version, networkName, waitConfirm, topics) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*fftypes.FFI) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool) error); ok { - r1 = rf(ctx, name, version, networkName, waitConfirm) + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool, []string) error); ok { + r1 = rf(ctx, name, version, networkName, waitConfirm, topics) } else { r1 = ret.Error(1) } From 18ea870384d43e3654a6dd56c06ba21077541af9 Mon Sep 17 00:00:00 2001 From: Kaijun Zhang Date: Mon, 13 Jul 2026 13:42:56 -0400 Subject: [PATCH 2/4] -update: custom topics Signed-off-by: Kaijun Zhang --- doc-site/docs/swagger/swagger.yaml | 40 ++++++++++++++----- .../route_post_contract_api_publish.go | 2 +- .../route_post_contract_interface_publish.go | 2 +- .../apiserver/route_post_new_contract_api.go | 2 +- .../route_post_new_contract_interface.go | 2 +- internal/apiserver/route_put_contract_api.go | 2 +- internal/coremsgs/en_api_translations.go | 2 +- internal/coremsgs/en_error_messages.go | 1 + internal/definitions/sender.go | 14 +++---- internal/definitions/sender_contracts_test.go | 25 ++++++++++++ 10 files changed, 68 insertions(+), 24 deletions(-) diff --git a/doc-site/docs/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml index 1d7ca1811a..3f750440f5 100644 --- a/doc-site/docs/swagger/swagger.yaml +++ b/doc-site/docs/swagger/swagger.yaml @@ -171,7 +171,9 @@ paths: name: publish schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -1807,7 +1809,9 @@ paths: name: confirm schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -2425,7 +2429,9 @@ paths: schema: example: "true" type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -3849,7 +3855,9 @@ paths: name: publish schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -4884,7 +4892,9 @@ paths: name: confirm schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -13756,7 +13766,9 @@ paths: name: publish schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -15652,7 +15664,9 @@ paths: name: confirm schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -16550,7 +16564,9 @@ paths: schema: example: "true" type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -18037,7 +18053,9 @@ paths: name: publish schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: @@ -19100,7 +19118,9 @@ paths: name: confirm schema: type: string - - description: Custom topics for ordering definition broadcast messages + - description: Custom topics to use for ordering of the definition broadcast + message, instead of the default system topic. Each topic must be a non-empty + string. Only used when the definition is published to the multiparty network in: query name: topics schema: diff --git a/internal/apiserver/route_post_contract_api_publish.go b/internal/apiserver/route_post_contract_api_publish.go index 3102529967..9714ff726c 100644 --- a/internal/apiserver/route_post_contract_api_publish.go +++ b/internal/apiserver/route_post_contract_api_publish.go @@ -35,7 +35,7 @@ var postContractAPIPublish = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostContractAPIPublish, JSONInputValue: func() interface{} { return &core.DefinitionPublish{} }, diff --git a/internal/apiserver/route_post_contract_interface_publish.go b/internal/apiserver/route_post_contract_interface_publish.go index c8607dd463..1d63f403f8 100644 --- a/internal/apiserver/route_post_contract_interface_publish.go +++ b/internal/apiserver/route_post_contract_interface_publish.go @@ -36,7 +36,7 @@ var postContractInterfacePublish = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostContractInterfacePublish, JSONInputValue: func() interface{} { return &core.DefinitionPublish{} }, diff --git a/internal/apiserver/route_post_new_contract_api.go b/internal/apiserver/route_post_new_contract_api.go index bfda155be4..fb40c6ce90 100644 --- a/internal/apiserver/route_post_new_contract_api.go +++ b/internal/apiserver/route_post_new_contract_api.go @@ -34,7 +34,7 @@ var postNewContractAPI = &ffapi.Route{ QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostNewContractAPI, JSONInputValue: func() interface{} { return &core.ContractAPI{} }, diff --git a/internal/apiserver/route_post_new_contract_interface.go b/internal/apiserver/route_post_new_contract_interface.go index e90adea813..c2172e00e8 100644 --- a/internal/apiserver/route_post_new_contract_interface.go +++ b/internal/apiserver/route_post_new_contract_interface.go @@ -34,7 +34,7 @@ var postNewContractInterface = &ffapi.Route{ QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostNewContractInterface, JSONInputValue: func() interface{} { return &fftypes.FFI{} }, diff --git a/internal/apiserver/route_put_contract_api.go b/internal/apiserver/route_put_contract_api.go index a3a37e7d33..a04521faff 100644 --- a/internal/apiserver/route_put_contract_api.go +++ b/internal/apiserver/route_put_contract_api.go @@ -36,7 +36,7 @@ var putContractAPI = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, - {Name: "topics", Description: coremsgs.APICustomTopicsQueryParam, IsArray: true}, + {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIParamsContractAPIID, JSONInputValue: func() interface{} { return &core.ContractAPI{} }, diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index 590e28c692..fb99ef52e5 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -217,5 +217,5 @@ var ( APISubscriptionStartSequenceID = ffm("api.startsequenceid", "The sequence ID in the raw event stream to start indexing through events from. Leave blank to start indexing from the most recent events") APISubscriptionEndSequenceID = ffm("api.endsequenceid", "The sequence ID in the raw event stream to stop indexing through events at. Leave blank to start indexing from the most recent events") - APICustomTopicsQueryParam = ffm("api.customTopicsQueryParam", "Custom topics for ordering definition broadcast messages") + APICustomTopicsParam = ffm("api.customTopicsParam", "Custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") ) diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index 7ca9bd83a0..2370255994 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -323,4 +323,5 @@ var ( MsgNodeMissingProfile = ffe("FF10482", "Node provided for check does not have a profile", 500) MsgInvalidCardanoAddress = ffe("FF10483", "Supplied cardano address is invalid", 400) MsgCardanoconnectRESTErr = ffe("FF10484", "Error from cardano connector: %s") + MsgEmptyCustomTopic = ffe("FF10485", "Custom topic at index %d is empty. Each custom topic for the definition broadcast message must be a non-empty string", 400) ) diff --git a/internal/definitions/sender.go b/internal/definitions/sender.go index 3dcf7ecfc2..094366cc38 100644 --- a/internal/definitions/sender.go +++ b/internal/definitions/sender.go @@ -159,15 +159,13 @@ func (ds *definitionSender) getSenderResolved(ctx context.Context, def core.Defi return wrapSendError(i18n.WrapError(ctx, err, coremsgs.MsgSerializationFailed)) } topics := fftypes.FFStringArray{def.Topic()} - - var filtered []string - for _, t := range customTopics { - if t != "" { - filtered = append(filtered, t) + if len(customTopics) > 0 { + for i, t := range customTopics { + if t == "" { + return wrapSendError(i18n.NewError(ctx, coremsgs.MsgEmptyCustomTopic, i)) + } } - } - if len(filtered) > 0 { - topics = fftypes.FFStringArray(filtered) + topics = fftypes.FFStringArray(customTopics) } dataValue := fftypes.JSONAnyPtrBytes(b) message := &core.MessageInOut{ diff --git a/internal/definitions/sender_contracts_test.go b/internal/definitions/sender_contracts_test.go index 7b7b856b40..8cc1f5ef46 100644 --- a/internal/definitions/sender_contracts_test.go +++ b/internal/definitions/sender_contracts_test.go @@ -258,6 +258,31 @@ func TestDefineFFICustomTopics(t *testing.T) { mms.AssertExpectations(t) } +// test that an empty custom topic is rejected with a descriptive validation error +func TestDefineFFIEmptyCustomTopic(t *testing.T) { + ds := newTestDefinitionSender(t) + defer ds.cleanup(t) + ds.multiparty = true + + ffi := &fftypes.FFI{ + Name: "ffi1", + Version: "1.0", + Published: true, + } + + ds.mdi.On("GetFFIByNetworkName", context.Background(), "ns1", "ffi1", "1.0").Return(nil, nil) + ds.mcm.On("ResolveFFI", context.Background(), ffi).Return(nil) + ds.mim.On("GetRootOrg", context.Background()).Return(&core.Identity{ + IdentityBase: core.IdentityBase{ + DID: "firefly:org1", + }, + }, nil) + ds.mim.On("ResolveInputSigningIdentity", context.Background(), mock.Anything).Return(nil) + + err := ds.DefineFFI(context.Background(), ffi, false, []string{"my-custom-topic", ""}) + assert.Regexp(t, "FF10485.*index 1", err) +} + func TestDefineContractAPIResolveFail(t *testing.T) { ds := newTestDefinitionSender(t) defer ds.cleanup(t) From 5bc44224058ac2f08c74a1e0ce179c77e97278b5 Mon Sep 17 00:00:00 2001 From: Kaijun Zhang Date: Wed, 15 Jul 2026 09:04:20 -0500 Subject: [PATCH 3/4] -feat: relocate topics into request body Signed-off-by: Kaijun Zhang --- doc-site/docs/swagger/swagger.yaml | 234 +++++++++++------- .../route_post_contract_api_publish.go | 3 +- .../route_post_contract_api_publish_test.go | 5 +- .../route_post_contract_interface_publish.go | 3 +- ...te_post_contract_interface_publish_test.go | 4 +- .../apiserver/route_post_new_contract_api.go | 8 +- .../route_post_new_contract_api_test.go | 5 +- .../route_post_new_contract_interface.go | 9 +- .../route_post_new_contract_interface_test.go | 5 +- internal/apiserver/route_put_contract_api.go | 8 +- .../apiserver/route_put_contract_api_test.go | 5 +- internal/coremsgs/en_api_translations.go | 2 - internal/coremsgs/en_struct_descriptions.go | 7 + pkg/core/contracts.go | 10 + pkg/core/definition.go | 3 +- 15 files changed, 192 insertions(+), 119 deletions(-) diff --git a/doc-site/docs/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml index 3f750440f5..5fd679f24a 100644 --- a/doc-site/docs/swagger/swagger.yaml +++ b/doc-site/docs/swagger/swagger.yaml @@ -171,15 +171,6 @@ paths: name: publish schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -224,6 +215,18 @@ paths: description: The published name of the API within the multiparty network type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -1809,15 +1812,6 @@ paths: name: confirm schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -1834,6 +1828,18 @@ paths: description: An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -2429,15 +2435,6 @@ paths: schema: example: "true" type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -2482,6 +2479,18 @@ paths: description: The published name of the API within the multiparty network type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -3855,15 +3864,6 @@ paths: name: publish schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -4028,6 +4028,18 @@ paths: description: The published name of the FFI within the multiparty network type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -4892,15 +4904,6 @@ paths: name: confirm schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -4917,6 +4920,18 @@ paths: description: An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -13766,15 +13781,6 @@ paths: name: publish schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -13819,6 +13825,18 @@ paths: description: The published name of the API within the multiparty network type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -15664,15 +15682,6 @@ paths: name: confirm schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -15689,6 +15698,18 @@ paths: description: An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -16564,15 +16585,6 @@ paths: schema: example: "true" type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -16617,6 +16629,18 @@ paths: description: The published name of the API within the multiparty network type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -18053,15 +18077,6 @@ paths: name: publish schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -18226,6 +18241,18 @@ paths: description: The published name of the FFI within the multiparty network type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -19118,15 +19145,6 @@ paths: name: confirm schema: type: string - - description: Custom topics to use for ordering of the definition broadcast - message, instead of the default system topic. Each topic must be a non-empty - string. Only used when the definition is published to the multiparty network - in: query - name: topics - schema: - items: - type: string - type: array - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) in: header @@ -19143,6 +19161,18 @@ paths: description: An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -36102,6 +36132,18 @@ paths: description: An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: @@ -45652,6 +45694,18 @@ paths: description: An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name type: string + topics: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default system + topic. Each topic must be a non-empty string. Only used when the + definition is published to the multiparty network + items: + description: An optional list of custom topics to use for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + type: string + type: array type: object required: true responses: diff --git a/internal/apiserver/route_post_contract_api_publish.go b/internal/apiserver/route_post_contract_api_publish.go index 9714ff726c..4f8474cfeb 100644 --- a/internal/apiserver/route_post_contract_api_publish.go +++ b/internal/apiserver/route_post_contract_api_publish.go @@ -35,7 +35,6 @@ var postContractAPIPublish = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostContractAPIPublish, JSONInputValue: func() interface{} { return &core.DefinitionPublish{} }, @@ -46,7 +45,7 @@ var postContractAPIPublish = &ffapi.Route{ waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) input := r.Input.(*core.DefinitionPublish) - return cr.or.DefinitionSender().PublishContractAPI(cr.ctx, cr.apiBaseURL, r.PP["apiName"], input.NetworkName, waitConfirm, r.QAP["topics"]) + return cr.or.DefinitionSender().PublishContractAPI(cr.ctx, cr.apiBaseURL, r.PP["apiName"], input.NetworkName, waitConfirm, input.Topics) }, }, } diff --git a/internal/apiserver/route_post_contract_api_publish_test.go b/internal/apiserver/route_post_contract_api_publish_test.go index 58847a747f..20980e53b4 100644 --- a/internal/apiserver/route_post_contract_api_publish_test.go +++ b/internal/apiserver/route_post_contract_api_publish_test.go @@ -22,6 +22,7 @@ import ( "net/http/httptest" "testing" + "github.com/hyperledger-firefly/common/pkg/fftypes" "github.com/hyperledger-firefly/firefly/mocks/definitionsmocks" "github.com/hyperledger-firefly/firefly/pkg/core" "github.com/stretchr/testify/assert" @@ -52,10 +53,10 @@ func TestPostContractAPIPublishWithTopics(t *testing.T) { o.On("Authorize", mock.Anything, mock.Anything).Return(nil) mds := &definitionsmocks.Sender{} o.On("DefinitionSender").Return(mds) - input := core.DefinitionPublish{NetworkName: "banana-net"} + input := core.DefinitionPublish{NetworkName: "banana-net", Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) - req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/apis/banana/publish?topics=my-topic", &buf) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/apis/banana/publish", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() api := &core.ContractAPI{} diff --git a/internal/apiserver/route_post_contract_interface_publish.go b/internal/apiserver/route_post_contract_interface_publish.go index 1d63f403f8..5b0c27c40e 100644 --- a/internal/apiserver/route_post_contract_interface_publish.go +++ b/internal/apiserver/route_post_contract_interface_publish.go @@ -36,7 +36,6 @@ var postContractInterfacePublish = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostContractInterfacePublish, JSONInputValue: func() interface{} { return &core.DefinitionPublish{} }, @@ -47,7 +46,7 @@ var postContractInterfacePublish = &ffapi.Route{ waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) input := r.Input.(*core.DefinitionPublish) - return cr.or.DefinitionSender().PublishFFI(cr.ctx, r.PP["name"], r.PP["version"], input.NetworkName, waitConfirm, r.QAP["topics"]) + return cr.or.DefinitionSender().PublishFFI(cr.ctx, r.PP["name"], r.PP["version"], input.NetworkName, waitConfirm, input.Topics) }, }, } diff --git a/internal/apiserver/route_post_contract_interface_publish_test.go b/internal/apiserver/route_post_contract_interface_publish_test.go index ffbbd31a8a..d8918ae717 100644 --- a/internal/apiserver/route_post_contract_interface_publish_test.go +++ b/internal/apiserver/route_post_contract_interface_publish_test.go @@ -53,10 +53,10 @@ func TestPostContractInterfacePublishWithTopics(t *testing.T) { o.On("Authorize", mock.Anything, mock.Anything).Return(nil) mds := &definitionsmocks.Sender{} o.On("DefinitionSender").Return(mds) - input := core.TokenPool{} + input := core.DefinitionPublish{Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) - req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces/ffi1/1.0/publish?topics=my-topic", &buf) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces/ffi1/1.0/publish", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() ffi := &fftypes.FFI{} diff --git a/internal/apiserver/route_post_new_contract_api.go b/internal/apiserver/route_post_new_contract_api.go index fb40c6ce90..e57eb54b2d 100644 --- a/internal/apiserver/route_post_new_contract_api.go +++ b/internal/apiserver/route_post_new_contract_api.go @@ -34,10 +34,9 @@ var postNewContractAPI = &ffapi.Route{ QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostNewContractAPI, - JSONInputValue: func() interface{} { return &core.ContractAPI{} }, + JSONInputValue: func() interface{} { return &core.ContractAPIInput{} }, JSONOutputValue: func() interface{} { return &core.ContractAPI{} }, JSONOutputCodes: []int{http.StatusOK, http.StatusAccepted}, Extensions: &coreExtensions{ @@ -47,10 +46,11 @@ var postNewContractAPI = &ffapi.Route{ CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) { waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) - api := r.Input.(*core.ContractAPI) + input := r.Input.(*core.ContractAPIInput) + api := &input.ContractAPI api.ID = nil api.Published = strings.EqualFold(r.QP["publish"], "true") - err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, r.QAP["topics"]) + err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, input.Topics) return api, err }, }, diff --git a/internal/apiserver/route_post_new_contract_api_test.go b/internal/apiserver/route_post_new_contract_api_test.go index efa9220092..0482b535c3 100644 --- a/internal/apiserver/route_post_new_contract_api_test.go +++ b/internal/apiserver/route_post_new_contract_api_test.go @@ -22,6 +22,7 @@ import ( "net/http/httptest" "testing" + "github.com/hyperledger-firefly/common/pkg/fftypes" "github.com/hyperledger-firefly/firefly/mocks/contractmocks" "github.com/hyperledger-firefly/firefly/mocks/definitionsmocks" "github.com/hyperledger-firefly/firefly/pkg/core" @@ -73,10 +74,10 @@ func TestPostNewContractAPIWithTopics(t *testing.T) { mds := &definitionsmocks.Sender{} o.On("Contracts").Return(&contractmocks.Manager{}) o.On("DefinitionSender").Return(mds) - input := core.Datatype{} + input := core.ContractAPIInput{Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) - req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/apis?topics=my-topic", &buf) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/apis", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() diff --git a/internal/apiserver/route_post_new_contract_interface.go b/internal/apiserver/route_post_new_contract_interface.go index c2172e00e8..9c3bdba806 100644 --- a/internal/apiserver/route_post_new_contract_interface.go +++ b/internal/apiserver/route_post_new_contract_interface.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger-firefly/common/pkg/fftypes" "github.com/hyperledger-firefly/firefly/internal/coremsgs" "github.com/hyperledger-firefly/firefly/internal/orchestrator" + "github.com/hyperledger-firefly/firefly/pkg/core" ) var postNewContractInterface = &ffapi.Route{ @@ -34,10 +35,9 @@ var postNewContractInterface = &ffapi.Route{ QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, - {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIEndpointsPostNewContractInterface, - JSONInputValue: func() interface{} { return &fftypes.FFI{} }, + JSONInputValue: func() interface{} { return &core.FFIInput{} }, JSONOutputValue: func() interface{} { return &fftypes.FFI{} }, JSONOutputCodes: []int{http.StatusOK}, Extensions: &coreExtensions{ @@ -47,9 +47,10 @@ var postNewContractInterface = &ffapi.Route{ CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) { waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) - ffi := r.Input.(*fftypes.FFI) + input := r.Input.(*core.FFIInput) + ffi := &input.FFI ffi.Published = strings.EqualFold(r.QP["publish"], "true") - err = cr.or.DefinitionSender().DefineFFI(cr.ctx, ffi, waitConfirm, r.QAP["topics"]) + err = cr.or.DefinitionSender().DefineFFI(cr.ctx, ffi, waitConfirm, input.Topics) return ffi, err }, }, diff --git a/internal/apiserver/route_post_new_contract_interface_test.go b/internal/apiserver/route_post_new_contract_interface_test.go index 86463ab07d..48bcebeb60 100644 --- a/internal/apiserver/route_post_new_contract_interface_test.go +++ b/internal/apiserver/route_post_new_contract_interface_test.go @@ -22,6 +22,7 @@ import ( "net/http/httptest" "testing" + "github.com/hyperledger-firefly/common/pkg/fftypes" "github.com/hyperledger-firefly/firefly/mocks/contractmocks" "github.com/hyperledger-firefly/firefly/mocks/definitionsmocks" "github.com/hyperledger-firefly/firefly/pkg/core" @@ -73,10 +74,10 @@ func TestPostNewContractInterfaceWithTopics(t *testing.T) { mds := &definitionsmocks.Sender{} o.On("Contracts").Return(&contractmocks.Manager{}) o.On("DefinitionSender").Return(mds) - input := core.Datatype{} + input := core.FFIInput{Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) - req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces?topics=my-topic", &buf) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() diff --git a/internal/apiserver/route_put_contract_api.go b/internal/apiserver/route_put_contract_api.go index a04521faff..9a516eaa1f 100644 --- a/internal/apiserver/route_put_contract_api.go +++ b/internal/apiserver/route_put_contract_api.go @@ -36,10 +36,9 @@ var putContractAPI = &ffapi.Route{ }, QueryParams: []*ffapi.QueryParam{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, - {Name: "topics", Description: coremsgs.APICustomTopicsParam, IsArray: true}, }, Description: coremsgs.APIParamsContractAPIID, - JSONInputValue: func() interface{} { return &core.ContractAPI{} }, + JSONInputValue: func() interface{} { return &core.ContractAPIInput{} }, JSONOutputValue: func() interface{} { return &core.ContractAPI{} }, JSONOutputCodes: []int{http.StatusOK, http.StatusAccepted}, Extensions: &coreExtensions{ @@ -49,10 +48,11 @@ var putContractAPI = &ffapi.Route{ CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) { waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) - api := r.Input.(*core.ContractAPI) + input := r.Input.(*core.ContractAPIInput) + api := &input.ContractAPI api.ID, err = fftypes.ParseUUID(cr.ctx, r.PP["id"]) if err == nil { - err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, r.QAP["topics"]) + err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, input.Topics) } return api, err }, diff --git a/internal/apiserver/route_put_contract_api_test.go b/internal/apiserver/route_put_contract_api_test.go index 1a48149c2c..2f86352d2c 100644 --- a/internal/apiserver/route_put_contract_api_test.go +++ b/internal/apiserver/route_put_contract_api_test.go @@ -22,6 +22,7 @@ import ( "net/http/httptest" "testing" + "github.com/hyperledger-firefly/common/pkg/fftypes" "github.com/hyperledger-firefly/firefly/mocks/contractmocks" "github.com/hyperledger-firefly/firefly/mocks/definitionsmocks" "github.com/hyperledger-firefly/firefly/pkg/core" @@ -73,10 +74,10 @@ func TestPutContractAPIWithTopics(t *testing.T) { mds := &definitionsmocks.Sender{} o.On("DefinitionSender").Return(mds) o.On("Contracts").Return(&contractmocks.Manager{}) - input := core.Datatype{} + input := core.ContractAPIInput{Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) - req := httptest.NewRequest("PUT", "/api/v1/namespaces/ns1/apis/99EEE458-037C-4C78-B66B-31E52F93D2E9?topics=my-topic", &buf) + req := httptest.NewRequest("PUT", "/api/v1/namespaces/ns1/apis/99EEE458-037C-4C78-B66B-31E52F93D2E9", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index fb99ef52e5..e7c4aa76e1 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -216,6 +216,4 @@ var ( APISubscriptionStartSequenceID = ffm("api.startsequenceid", "The sequence ID in the raw event stream to start indexing through events from. Leave blank to start indexing from the most recent events") APISubscriptionEndSequenceID = ffm("api.endsequenceid", "The sequence ID in the raw event stream to stop indexing through events at. Leave blank to start indexing from the most recent events") - - APICustomTopicsParam = ffm("api.customTopicsParam", "Custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") ) diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index 8411e61145..d6486ff6b1 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -739,4 +739,11 @@ var ( // DefinitionPublish field descriptions DefinitionPublishNetworkName = ffm("DefinitionPublish.networkName", "An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name") + DefinitionPublishTopics = ffm("DefinitionPublish.topics", "An optional list of custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") + + // ContractAPIInput field descriptions + ContractAPIInputTopics = ffm("ContractAPIInput.topics", "An optional list of custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") + + // FFIInput field descriptions + FFIInputTopics = ffm("FFIInput.topics", "An optional list of custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") ) diff --git a/pkg/core/contracts.go b/pkg/core/contracts.go index c164fdb50b..bc57f24c28 100644 --- a/pkg/core/contracts.go +++ b/pkg/core/contracts.go @@ -72,6 +72,16 @@ type ContractAPI struct { Published bool `ffstruct:"ContractAPI" json:"published" ffexcludeinput:"true"` } +type ContractAPIInput struct { + ContractAPI + Topics fftypes.FFStringArray `ffstruct:"ContractAPIInput" json:"topics,omitempty" ffexcludeoutput:"true"` +} + +type FFIInput struct { + fftypes.FFI + Topics fftypes.FFStringArray `ffstruct:"FFIInput" json:"topics,omitempty" ffexcludeoutput:"true"` +} + func (c *ContractAPI) Validate(ctx context.Context) (err error) { if err = fftypes.ValidateFFNameField(ctx, c.Namespace, "namespace"); err != nil { return err diff --git a/pkg/core/definition.go b/pkg/core/definition.go index 72d618a491..7f2d98ec60 100644 --- a/pkg/core/definition.go +++ b/pkg/core/definition.go @@ -27,5 +27,6 @@ type Definition interface { } type DefinitionPublish struct { - NetworkName string `ffstruct:"DefinitionPublish" json:"networkName,omitempty"` + NetworkName string `ffstruct:"DefinitionPublish" json:"networkName,omitempty"` + Topics fftypes.FFStringArray `ffstruct:"DefinitionPublish" json:"topics,omitempty"` } From 70f34c7fa6974eb83c23d533a8383496fc5056d2 Mon Sep 17 00:00:00 2001 From: Kaijun Zhang Date: Fri, 17 Jul 2026 16:03:55 -0400 Subject: [PATCH 4/4] -update: persist topic in FFI Signed-off-by: Kaijun Zhang --- .../000118_add_definition_topics.down.sql | 4 + .../000118_add_definition_topics.up.sql | 4 + .../000118_add_definition_topics.down.sql | 2 + .../000118_add_definition_topics.up.sql | 2 + doc-site/docs/reference/types/contractapi.md | 1 + doc-site/docs/reference/types/ffi.md | 1 + doc-site/docs/swagger/swagger.yaml | 432 +++++++++++++++++- go.mod | 2 + go.sum | 2 - .../apiserver/route_post_new_contract_api.go | 7 +- .../route_post_new_contract_api_test.go | 2 +- .../route_post_new_contract_interface.go | 8 +- .../route_post_new_contract_interface_test.go | 2 +- internal/apiserver/route_put_contract_api.go | 7 +- .../apiserver/route_put_contract_api_test.go | 2 +- internal/coremsgs/en_struct_descriptions.go | 8 +- .../database/sqlcommon/contractapis_sql.go | 4 + .../sqlcommon/contractapis_sql_test.go | 14 +- internal/database/sqlcommon/ffi_sql.go | 4 + internal/database/sqlcommon/ffi_sql_test.go | 4 +- internal/definitions/sender_contracts.go | 14 + pkg/core/contracts.go | 11 +- pkg/database/plugin.go | 2 + 23 files changed, 484 insertions(+), 55 deletions(-) create mode 100644 db/migrations/postgres/000118_add_definition_topics.down.sql create mode 100644 db/migrations/postgres/000118_add_definition_topics.up.sql create mode 100644 db/migrations/sqlite/000118_add_definition_topics.down.sql create mode 100644 db/migrations/sqlite/000118_add_definition_topics.up.sql diff --git a/db/migrations/postgres/000118_add_definition_topics.down.sql b/db/migrations/postgres/000118_add_definition_topics.down.sql new file mode 100644 index 0000000000..167334c13d --- /dev/null +++ b/db/migrations/postgres/000118_add_definition_topics.down.sql @@ -0,0 +1,4 @@ +BEGIN; +ALTER TABLE ffi DROP COLUMN topics; +ALTER TABLE contractapis DROP COLUMN topics; +COMMIT; diff --git a/db/migrations/postgres/000118_add_definition_topics.up.sql b/db/migrations/postgres/000118_add_definition_topics.up.sql new file mode 100644 index 0000000000..251639c648 --- /dev/null +++ b/db/migrations/postgres/000118_add_definition_topics.up.sql @@ -0,0 +1,4 @@ +BEGIN; +ALTER TABLE ffi ADD COLUMN topics VARCHAR(1024); +ALTER TABLE contractapis ADD COLUMN topics VARCHAR(1024); +COMMIT; diff --git a/db/migrations/sqlite/000118_add_definition_topics.down.sql b/db/migrations/sqlite/000118_add_definition_topics.down.sql new file mode 100644 index 0000000000..6323c29954 --- /dev/null +++ b/db/migrations/sqlite/000118_add_definition_topics.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE ffi DROP COLUMN topics; +ALTER TABLE contractapis DROP COLUMN topics; diff --git a/db/migrations/sqlite/000118_add_definition_topics.up.sql b/db/migrations/sqlite/000118_add_definition_topics.up.sql new file mode 100644 index 0000000000..0bcab8fc8e --- /dev/null +++ b/db/migrations/sqlite/000118_add_definition_topics.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE ffi ADD COLUMN topics VARCHAR(1024); +ALTER TABLE contractapis ADD COLUMN topics VARCHAR(1024); diff --git a/doc-site/docs/reference/types/contractapi.md b/doc-site/docs/reference/types/contractapi.md index c4befcdcd2..0e1e7eec78 100644 --- a/doc-site/docs/reference/types/contractapi.md +++ b/doc-site/docs/reference/types/contractapi.md @@ -39,6 +39,7 @@ title: ContractAPI | `message` | The UUID of the broadcast message that was used to publish this API to the network | [`UUID`](simpletypes.md#uuid) | | `urls` | The URLs to use to access the API | [`ContractURLs`](#contracturls) | | `published` | Indicates if the API is published to other members of the multiparty network | `bool` | +| `topics` | An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network | `string[]` | ## FFIReference diff --git a/doc-site/docs/reference/types/ffi.md b/doc-site/docs/reference/types/ffi.md index 1d5b4692eb..d0a30e2625 100644 --- a/doc-site/docs/reference/types/ffi.md +++ b/doc-site/docs/reference/types/ffi.md @@ -112,6 +112,7 @@ title: FFI | `events` | An array of smart contract event definitions | [`FFIEvent[]`](#ffievent) | | `errors` | An array of smart contract error definitions | [`FFIError[]`](#ffierror) | | `published` | Indicates if the FFI is published to other members of the multiparty network | `bool` | +| `topics` | An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network | `string[]` | ## FFIMethod diff --git a/doc-site/docs/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml index 5fd679f24a..f9bbcc9f75 100644 --- a/doc-site/docs/swagger/swagger.yaml +++ b/doc-site/docs/swagger/swagger.yaml @@ -41,6 +41,11 @@ paths: name: published schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: topics + schema: + type: string - description: Sort field. For multi-field sort use comma separated values (or multiple query values) with '-' prefix for descending in: query @@ -133,6 +138,19 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty + network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -216,12 +234,12 @@ paths: network type: string topics: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network items: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network @@ -285,6 +303,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -357,6 +387,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -479,6 +521,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -759,6 +813,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -2083,6 +2149,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -2329,6 +2407,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -2480,12 +2570,12 @@ paths: network type: string topics: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network items: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network @@ -2549,6 +2639,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -2621,6 +2723,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -3548,6 +3662,11 @@ paths: name: published schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: topics + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: version @@ -3837,6 +3956,19 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty + network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -4029,12 +4161,12 @@ paths: network type: string topics: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network items: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network @@ -4287,6 +4419,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -4589,6 +4733,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -4871,6 +5027,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -5175,6 +5343,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -5421,6 +5601,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -5714,6 +5906,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -13644,6 +13848,11 @@ paths: name: published schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: topics + schema: + type: string - description: Sort field. For multi-field sort use comma separated values (or multiple query values) with '-' prefix for descending in: query @@ -13736,6 +13945,19 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty + network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -13826,12 +14048,12 @@ paths: network type: string topics: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network items: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network @@ -13895,6 +14117,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -13967,6 +14201,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -14103,6 +14349,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -14390,6 +14648,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -15953,6 +16223,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -16199,6 +16481,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -16630,12 +16924,12 @@ paths: network type: string topics: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network items: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network @@ -16699,6 +16993,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -16771,6 +17077,18 @@ paths: description: Indicates if the API is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array urls: description: The URLs to use to access the API properties: @@ -17754,6 +18072,11 @@ paths: name: published schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: topics + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: version @@ -18043,6 +18366,19 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty + network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -18242,12 +18578,12 @@ paths: network type: string topics: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network items: - description: An optional list of custom topics to use for ordering + description: An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network @@ -18500,6 +18836,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -18816,6 +19164,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -19105,6 +19465,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -19416,6 +19788,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -19662,6 +20046,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged @@ -19962,6 +20358,18 @@ paths: description: Indicates if the FFI is published to other members of the multiparty network type: boolean + topics: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only used + when the definition is published to the multiparty network + items: + description: An optional list of custom topics used for ordering + of the definition broadcast message, instead of the default + system topic. Each topic must be a non-empty string. Only + used when the definition is published to the multiparty network + type: string + type: array version: description: A version for the FFI - use of semantic versioning such as 'v1.0.1' is encouraged diff --git a/go.mod b/go.mod index b62d1f5c76..574877d3d7 100644 --- a/go.mod +++ b/go.mod @@ -97,3 +97,5 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/hyperledger-firefly/common => ../firefly-common diff --git a/go.sum b/go.sum index d2afb93586..09cdb092df 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hyperledger-firefly/common v1.6.3 h1:z9Jm6ttwy7AJvrHw0FbjtPT5wLG8n3K8Rrf3MUkifdY= -github.com/hyperledger-firefly/common v1.6.3/go.mod h1:UiHXY2QdM8UUYHDpFpOAt2hwrVL559G8wZ3b68kAEx8= github.com/hyperledger-firefly/signer v1.2.1 h1:YmnCfiOPhrP/6WIGt5KwV3wiDEI61k7h282MpagSNnI= github.com/hyperledger-firefly/signer v1.2.1/go.mod h1:qLBLnXZZHk83x68WLvDjx8q80+6lQzx57FPeRyI6jmo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/internal/apiserver/route_post_new_contract_api.go b/internal/apiserver/route_post_new_contract_api.go index e57eb54b2d..45870f4af1 100644 --- a/internal/apiserver/route_post_new_contract_api.go +++ b/internal/apiserver/route_post_new_contract_api.go @@ -36,7 +36,7 @@ var postNewContractAPI = &ffapi.Route{ {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, }, Description: coremsgs.APIEndpointsPostNewContractAPI, - JSONInputValue: func() interface{} { return &core.ContractAPIInput{} }, + JSONInputValue: func() interface{} { return &core.ContractAPI{} }, JSONOutputValue: func() interface{} { return &core.ContractAPI{} }, JSONOutputCodes: []int{http.StatusOK, http.StatusAccepted}, Extensions: &coreExtensions{ @@ -46,11 +46,10 @@ var postNewContractAPI = &ffapi.Route{ CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) { waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) - input := r.Input.(*core.ContractAPIInput) - api := &input.ContractAPI + api := r.Input.(*core.ContractAPI) api.ID = nil api.Published = strings.EqualFold(r.QP["publish"], "true") - err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, input.Topics) + err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, api.Topics) return api, err }, }, diff --git a/internal/apiserver/route_post_new_contract_api_test.go b/internal/apiserver/route_post_new_contract_api_test.go index 0482b535c3..34abd1002d 100644 --- a/internal/apiserver/route_post_new_contract_api_test.go +++ b/internal/apiserver/route_post_new_contract_api_test.go @@ -74,7 +74,7 @@ func TestPostNewContractAPIWithTopics(t *testing.T) { mds := &definitionsmocks.Sender{} o.On("Contracts").Return(&contractmocks.Manager{}) o.On("DefinitionSender").Return(mds) - input := core.ContractAPIInput{Topics: fftypes.FFStringArray{"my-topic"}} + input := core.ContractAPI{Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/apis", &buf) diff --git a/internal/apiserver/route_post_new_contract_interface.go b/internal/apiserver/route_post_new_contract_interface.go index 9c3bdba806..7e15ee90db 100644 --- a/internal/apiserver/route_post_new_contract_interface.go +++ b/internal/apiserver/route_post_new_contract_interface.go @@ -24,7 +24,6 @@ import ( "github.com/hyperledger-firefly/common/pkg/fftypes" "github.com/hyperledger-firefly/firefly/internal/coremsgs" "github.com/hyperledger-firefly/firefly/internal/orchestrator" - "github.com/hyperledger-firefly/firefly/pkg/core" ) var postNewContractInterface = &ffapi.Route{ @@ -37,7 +36,7 @@ var postNewContractInterface = &ffapi.Route{ {Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true}, }, Description: coremsgs.APIEndpointsPostNewContractInterface, - JSONInputValue: func() interface{} { return &core.FFIInput{} }, + JSONInputValue: func() interface{} { return &fftypes.FFI{} }, JSONOutputValue: func() interface{} { return &fftypes.FFI{} }, JSONOutputCodes: []int{http.StatusOK}, Extensions: &coreExtensions{ @@ -47,10 +46,9 @@ var postNewContractInterface = &ffapi.Route{ CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) { waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) - input := r.Input.(*core.FFIInput) - ffi := &input.FFI + ffi := r.Input.(*fftypes.FFI) ffi.Published = strings.EqualFold(r.QP["publish"], "true") - err = cr.or.DefinitionSender().DefineFFI(cr.ctx, ffi, waitConfirm, input.Topics) + err = cr.or.DefinitionSender().DefineFFI(cr.ctx, ffi, waitConfirm, ffi.Topics) return ffi, err }, }, diff --git a/internal/apiserver/route_post_new_contract_interface_test.go b/internal/apiserver/route_post_new_contract_interface_test.go index 48bcebeb60..d34bf500cb 100644 --- a/internal/apiserver/route_post_new_contract_interface_test.go +++ b/internal/apiserver/route_post_new_contract_interface_test.go @@ -74,7 +74,7 @@ func TestPostNewContractInterfaceWithTopics(t *testing.T) { mds := &definitionsmocks.Sender{} o.On("Contracts").Return(&contractmocks.Manager{}) o.On("DefinitionSender").Return(mds) - input := core.FFIInput{Topics: fftypes.FFStringArray{"my-topic"}} + input := fftypes.FFI{Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces", &buf) diff --git a/internal/apiserver/route_put_contract_api.go b/internal/apiserver/route_put_contract_api.go index 9a516eaa1f..7d752dca7b 100644 --- a/internal/apiserver/route_put_contract_api.go +++ b/internal/apiserver/route_put_contract_api.go @@ -38,7 +38,7 @@ var putContractAPI = &ffapi.Route{ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"}, }, Description: coremsgs.APIParamsContractAPIID, - JSONInputValue: func() interface{} { return &core.ContractAPIInput{} }, + JSONInputValue: func() interface{} { return &core.ContractAPI{} }, JSONOutputValue: func() interface{} { return &core.ContractAPI{} }, JSONOutputCodes: []int{http.StatusOK, http.StatusAccepted}, Extensions: &coreExtensions{ @@ -48,11 +48,10 @@ var putContractAPI = &ffapi.Route{ CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) { waitConfirm := strings.EqualFold(r.QP["confirm"], "true") r.SuccessStatus = syncRetcode(waitConfirm) - input := r.Input.(*core.ContractAPIInput) - api := &input.ContractAPI + api := r.Input.(*core.ContractAPI) api.ID, err = fftypes.ParseUUID(cr.ctx, r.PP["id"]) if err == nil { - err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, input.Topics) + err = cr.or.DefinitionSender().DefineContractAPI(cr.ctx, cr.apiBaseURL, api, waitConfirm, api.Topics) } return api, err }, diff --git a/internal/apiserver/route_put_contract_api_test.go b/internal/apiserver/route_put_contract_api_test.go index 2f86352d2c..92826d5572 100644 --- a/internal/apiserver/route_put_contract_api_test.go +++ b/internal/apiserver/route_put_contract_api_test.go @@ -74,7 +74,7 @@ func TestPutContractAPIWithTopics(t *testing.T) { mds := &definitionsmocks.Sender{} o.On("DefinitionSender").Return(mds) o.On("Contracts").Return(&contractmocks.Manager{}) - input := core.ContractAPIInput{Topics: fftypes.FFStringArray{"my-topic"}} + input := core.ContractAPI{Topics: fftypes.FFStringArray{"my-topic"}} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) req := httptest.NewRequest("PUT", "/api/v1/namespaces/ns1/apis/99EEE458-037C-4C78-B66B-31E52F93D2E9", &buf) diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index d6486ff6b1..89df14899c 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -232,6 +232,7 @@ var ( ContractAPIMessage = ffm("ContractAPI.message", "The UUID of the broadcast message that was used to publish this API to the network") ContractAPIURLs = ffm("ContractAPI.urls", "The URLs to use to access the API") ContractAPIPublished = ffm("ContractAPI.published", "Indicates if the API is published to other members of the multiparty network") + ContractAPITopics = ffm("ContractAPI.topics", "An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") // ContractURLs field descriptions ContractURLsAPI = ffm("ContractURLs.api", "The URL to use to invoke the API") @@ -255,6 +256,7 @@ var ( FFIEvents = ffm("FFI.events", "An array of smart contract event definitions") FFIErrors = ffm("FFI.errors", "An array of smart contract error definitions") FFIPublished = ffm("FFI.published", "Indicates if the FFI is published to other members of the multiparty network") + FFITopics = ffm("FFI.topics", "An optional list of custom topics used for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") // FFIMethod field descriptions FFIMethodID = ffm("FFIMethod.id", "The UUID of the FFI method definition") @@ -740,10 +742,4 @@ var ( // DefinitionPublish field descriptions DefinitionPublishNetworkName = ffm("DefinitionPublish.networkName", "An optional name to be used for publishing this definition to the multiparty network, which may differ from the local name") DefinitionPublishTopics = ffm("DefinitionPublish.topics", "An optional list of custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") - - // ContractAPIInput field descriptions - ContractAPIInputTopics = ffm("ContractAPIInput.topics", "An optional list of custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") - - // FFIInput field descriptions - FFIInputTopics = ffm("FFIInput.topics", "An optional list of custom topics to use for ordering of the definition broadcast message, instead of the default system topic. Each topic must be a non-empty string. Only used when the definition is published to the multiparty network") ) diff --git a/internal/database/sqlcommon/contractapis_sql.go b/internal/database/sqlcommon/contractapis_sql.go index cc6b71634d..c0b466a0ee 100644 --- a/internal/database/sqlcommon/contractapis_sql.go +++ b/internal/database/sqlcommon/contractapis_sql.go @@ -41,6 +41,7 @@ var ( "namespace", "message_id", "published", + "topics", } contractAPIsFilterFieldMap = map[string]string{ "interface": "interface_id", @@ -68,6 +69,7 @@ func (s *SQLCommon) attemptContractAPIUpdate(ctx context.Context, tx *dbsql.TXWr Set("network_name", networkName). Set("message_id", api.Message). Set("published", api.Published). + Set("topics", api.Topics). Where(sq.Eq{"id": api.ID}), func() { s.callbacks.UUIDCollectionNSEvent(database.CollectionContractAPIs, core.ChangeEventTypeUpdated, api.Namespace, api.ID) @@ -93,6 +95,7 @@ func (s *SQLCommon) setContractAPIInsertValues(query sq.InsertBuilder, api *core api.Namespace, api.Message, api.Published, + api.Topics, ) } @@ -202,6 +205,7 @@ func (s *SQLCommon) contractAPIResult(ctx context.Context, row *sql.Rows) (*core &api.Namespace, &api.Message, &api.Published, + &api.Topics, ) if networkName != nil { api.NetworkName = *networkName diff --git a/internal/database/sqlcommon/contractapis_sql_test.go b/internal/database/sqlcommon/contractapis_sql_test.go index 1da2f2bfb6..73b3b2238b 100644 --- a/internal/database/sqlcommon/contractapis_sql_test.go +++ b/internal/database/sqlcommon/contractapis_sql_test.go @@ -213,8 +213,8 @@ func TestContractAPIDBNoRows(t *testing.T) { func TestGetContractAPIs(t *testing.T) { fb := database.ContractAPIQueryFactory.NewFilter(context.Background()) s, mock := newMockProvider().init() - rows := sqlmock.NewRows([]string{"id", "interface_id", "location", "name", "network_name", "namespace", "message_id", "published"}). - AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "8fcc4938-7d8b-4c00-a71b-1b46837c8ab1", nil, "banana", "banana", "ns1", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", true) + rows := sqlmock.NewRows([]string{"id", "interface_id", "location", "name", "network_name", "namespace", "message_id", "published", "topics"}). + AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "8fcc4938-7d8b-4c00-a71b-1b46837c8ab1", nil, "banana", "banana", "ns1", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", true, "topic1,topic2") mock.ExpectQuery("SELECT .*").WillReturnRows(rows) _, _, err := s.GetContractAPIs(context.Background(), "ns1", fb.And()) assert.NoError(t, err) @@ -240,9 +240,9 @@ func TestGetContractAPIsQueryFail(t *testing.T) { func TestGetContractAPIsQueryResultFail(t *testing.T) { fb := database.ContractAPIQueryFactory.NewFilter(context.Background()) s, mock := newMockProvider().init() - rows := sqlmock.NewRows([]string{"id", "interface_id", "location", "name", "network_name", "namespace", "message_id", "published"}). - AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "8fcc4938-7d8b-4c00-a71b-1b46837c8ab1", nil, "apple", "apple", "ns1", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false). - AddRow("69851ca3-e9f9-489b-8731-dc6a7d990291", "4db4952e-4669-4243-a387-8f0f609e92bd", nil, "orange", "orange", nil, "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false) + rows := sqlmock.NewRows([]string{"id", "interface_id", "location", "name", "network_name", "namespace", "message_id", "published", "topics"}). + AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "8fcc4938-7d8b-4c00-a71b-1b46837c8ab1", nil, "apple", "apple", "ns1", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false, "topic1"). + AddRow("69851ca3-e9f9-489b-8731-dc6a7d990291", "4db4952e-4669-4243-a387-8f0f609e92bd", nil, "orange", "orange", nil, "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false, nil) mock.ExpectQuery("SELECT .*").WillReturnRows(rows) _, _, err := s.GetContractAPIs(context.Background(), "ns1", fb.And()) assert.Regexp(t, "FF10121", err) @@ -251,8 +251,8 @@ func TestGetContractAPIsQueryResultFail(t *testing.T) { func TestGetContractAPIByName(t *testing.T) { s, mock := newMockProvider().init() - rows := sqlmock.NewRows([]string{"id", "interface_id", "location", "name", "network_name", "namespace", "message_id", "published"}). - AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "8fcc4938-7d8b-4c00-a71b-1b46837c8ab1", nil, "banana", "banana", "ns1", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", true) + rows := sqlmock.NewRows([]string{"id", "interface_id", "location", "name", "network_name", "namespace", "message_id", "published", "topics"}). + AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "8fcc4938-7d8b-4c00-a71b-1b46837c8ab1", nil, "banana", "banana", "ns1", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", true, "topic1,topic2") mock.ExpectQuery("SELECT .*").WillReturnRows(rows) api, err := s.GetContractAPIByName(context.Background(), "ns1", "banana") assert.NotNil(t, api) diff --git a/internal/database/sqlcommon/ffi_sql.go b/internal/database/sqlcommon/ffi_sql.go index 0c8e62faa8..0c3595ed12 100644 --- a/internal/database/sqlcommon/ffi_sql.go +++ b/internal/database/sqlcommon/ffi_sql.go @@ -41,6 +41,7 @@ var ( "description", "message_id", "published", + "topics", } ffiFilterFieldMap = map[string]string{ "message": "message_id", @@ -63,6 +64,7 @@ func (s *SQLCommon) attemptFFIUpdate(ctx context.Context, tx *dbsql.TXWrapper, f Set("description", ffi.Description). Set("message_id", ffi.Message). Set("published", ffi.Published). + Set("topics", ffi.Topics). Where(sq.Eq{"id": ffi.ID}), func() { s.callbacks.UUIDCollectionNSEvent(database.CollectionFFIs, core.ChangeEventTypeUpdated, ffi.Namespace, ffi.ID) @@ -84,6 +86,7 @@ func (s *SQLCommon) setFFIInsertValues(query sq.InsertBuilder, ffi *fftypes.FFI) ffi.Description, ffi.Message, ffi.Published, + ffi.Topics, ) } @@ -191,6 +194,7 @@ func (s *SQLCommon) ffiResult(ctx context.Context, row *sql.Rows) (*fftypes.FFI, &ffi.Description, &ffi.Message, &ffi.Published, + &ffi.Topics, ) if networkName != nil { ffi.NetworkName = *networkName diff --git a/internal/database/sqlcommon/ffi_sql_test.go b/internal/database/sqlcommon/ffi_sql_test.go index 811a4199b4..4e226a43d6 100644 --- a/internal/database/sqlcommon/ffi_sql_test.go +++ b/internal/database/sqlcommon/ffi_sql_test.go @@ -227,7 +227,7 @@ func TestGetFFIs(t *testing.T) { fb := database.FFIQueryFactory.NewFilter(context.Background()) s, mock := newMockProvider().init() rows := sqlmock.NewRows(ffiColumns). - AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "ns1", "math", "math", "v1.0.0", "super mathy things", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false) + AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "ns1", "math", "math", "v1.0.0", "super mathy things", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false, "topic1,topic2") mock.ExpectQuery("SELECT .*").WillReturnRows(rows) _, _, err := s.GetFFIs(context.Background(), "ns1", fb.And()) assert.NoError(t, err) @@ -265,7 +265,7 @@ func TestGetFFIsQueryResultFail(t *testing.T) { func TestGetFFI(t *testing.T) { s, mock := newMockProvider().init() rows := sqlmock.NewRows(ffiColumns). - AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "ns1", "math", "math", "v1.0.0", "super mathy things", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false) + AddRow("7e2c001c-e270-4fd7-9e82-9dacee843dc2", "ns1", "math", "math", "v1.0.0", "super mathy things", "acfe07a2-117f-46b7-8d47-e3beb7cc382f", false, "topic1,topic2") mock.ExpectQuery("SELECT .*").WillReturnRows(rows) ffi, err := s.GetFFI(context.Background(), "ns1", "math", "v1.0.0") assert.NoError(t, err) diff --git a/internal/definitions/sender_contracts.go b/internal/definitions/sender_contracts.go index c1ec4cb131..9890df0a71 100644 --- a/internal/definitions/sender_contracts.go +++ b/internal/definitions/sender_contracts.go @@ -106,6 +106,15 @@ func (ds *definitionSender) PublishFFI(ctx context.Context, name, version, netwo return i18n.NewError(ctx, coremsgs.MsgAlreadyPublished) } ffi.NetworkName = networkName + // If user has explicitly defined a new set of topics at publish time + // we honor those and override the existing topics stored in FFI + // If not (i.e. when the new topics is empty), we fallback to the topics + // defined in the FFI + if len(topics) > 0 { + ffi.Topics = topics + } else { + topics = ffi.Topics + } sender = ds.getFFISender(ctx, ffi, topics) if sender.err != nil { return sender.err @@ -198,6 +207,11 @@ func (ds *definitionSender) PublishContractAPI(ctx context.Context, httpServerUR return i18n.NewError(ctx, coremsgs.MsgAlreadyPublished) } api.NetworkName = networkName + if len(topics) > 0 { + api.Topics = topics + } else { + topics = api.Topics + } sender = ds.getContractAPISender(ctx, httpServerURL, api, topics) if sender.err != nil { return sender.err diff --git a/pkg/core/contracts.go b/pkg/core/contracts.go index bc57f24c28..420eb0aa13 100644 --- a/pkg/core/contracts.go +++ b/pkg/core/contracts.go @@ -70,16 +70,7 @@ type ContractAPI struct { Message *fftypes.UUID `ffstruct:"ContractAPI" json:"message,omitempty" ffexcludeinput:"true"` URLs ContractURLs `ffstruct:"ContractAPI" json:"urls" ffexcludeinput:"true"` Published bool `ffstruct:"ContractAPI" json:"published" ffexcludeinput:"true"` -} - -type ContractAPIInput struct { - ContractAPI - Topics fftypes.FFStringArray `ffstruct:"ContractAPIInput" json:"topics,omitempty" ffexcludeoutput:"true"` -} - -type FFIInput struct { - fftypes.FFI - Topics fftypes.FFStringArray `ffstruct:"FFIInput" json:"topics,omitempty" ffexcludeoutput:"true"` + Topics fftypes.FFStringArray `ffstruct:"ContractAPI" json:"topics,omitempty"` } func (c *ContractAPI) Validate(ctx context.Context) (err error) { diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index 2d7e8d3b79..68ab29b8a0 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -1022,6 +1022,7 @@ var FFIQueryFactory = &ffapi.QueryFields{ "networkname": &ffapi.StringField{}, "version": &ffapi.StringField{}, "published": &ffapi.BoolField{}, + "topics": &ffapi.FFStringArrayField{}, } // FFIMethodQueryFactory filter fields for contract methods @@ -1086,4 +1087,5 @@ var ContractAPIQueryFactory = &ffapi.QueryFields{ "networkname": &ffapi.StringField{}, "interface": &ffapi.UUIDField{}, "published": &ffapi.BoolField{}, + "topics": &ffapi.FFStringArrayField{}, }