From 5a9a6dc7213458e9baf5983d19f1f8b50b485672 Mon Sep 17 00:00:00 2001 From: agelloz Date: Thu, 26 May 2022 20:46:10 +0200 Subject: [PATCH] fix(swagger): first --- pkg/api/api.go | 6 +- pkg/api/controllers/account_controller.go | 20 +- .../controllers/account_controller_test.go | 99 +++-- pkg/api/controllers/base_controller.go | 1 - pkg/api/controllers/config_controller.go | 5 - pkg/api/controllers/ledger_controller.go | 2 - pkg/api/controllers/script_controller.go | 4 +- pkg/api/controllers/script_controller_test.go | 4 +- pkg/api/controllers/swagger.yaml | 390 +++++++++--------- pkg/api/controllers/transaction_controller.go | 19 +- .../transaction_controller_test.go | 62 ++- pkg/api/internal/testing.go | 6 +- pkg/api/middlewares/ledger_middleware.go | 3 - pkg/api/routes/routes.go | 17 +- pkg/core/transaction.go | 1 - pkg/ledger/executor_test.go | 4 +- pkg/ledger/ledger.go | 128 ++---- pkg/ledger/ledger_test.go | 35 +- pkg/ledger/query/query.go | 6 + .../opentelemetrytraces/storage.go | 24 +- .../opentelemetrytraces/storage_test.go | 16 +- pkg/storage/sqlstorage/accounts.go | 41 +- pkg/storage/sqlstorage/mapping.go | 37 +- pkg/storage/sqlstorage/migrations_test.go | 2 +- pkg/storage/sqlstorage/sqlite.go | 6 +- pkg/storage/sqlstorage/store_bench_test.go | 8 +- pkg/storage/sqlstorage/store_test.go | 36 +- pkg/storage/sqlstorage/transactions.go | 131 +++--- pkg/storage/storage.go | 12 +- 29 files changed, 564 insertions(+), 561 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 21604f0970..bf02dfb358 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -11,7 +11,6 @@ import ( "go.uber.org/fx" ) -// API struct type API struct { handler *gin.Engine } @@ -20,10 +19,7 @@ func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.handler.ServeHTTP(w, r) } -// NewAPI -func NewAPI( - routes *routes.Routes, -) *API { +func NewAPI(routes *routes.Routes) *API { gin.SetMode(gin.ReleaseMode) h := &API{ handler: routes.Engine(), diff --git a/pkg/api/controllers/account_controller.go b/pkg/api/controllers/account_controller.go index ce871b2c27..19a29f582e 100644 --- a/pkg/api/controllers/account_controller.go +++ b/pkg/api/controllers/account_controller.go @@ -11,12 +11,10 @@ import ( "github.com/numary/ledger/pkg/ledger/query" ) -// AccountController - type AccountController struct { BaseController } -// NewAccountController - func NewAccountController() AccountController { return AccountController{} } @@ -26,11 +24,8 @@ func (ctl *AccountController) CountAccounts(c *gin.Context) { count, err := l.(*ledger.Ledger).CountAccounts( c.Request.Context(), - query.After(c.Query("after")), query.Address(c.Query("address")), - func(q *query.Query) { - q.Params["metadata"] = c.QueryMap("metadata") - }, + query.Metadata(c.QueryMap("metadata")), ) if err != nil { ResponseError(c, err) @@ -43,13 +38,11 @@ func (ctl *AccountController) CountAccounts(c *gin.Context) { func (ctl *AccountController) GetAccounts(c *gin.Context) { l, _ := c.Get("ledger") - cursor, err := l.(*ledger.Ledger).FindAccounts( + cursor, err := l.(*ledger.Ledger).GetAccounts( c.Request.Context(), query.After(c.Query("after")), query.Address(c.Query("address")), - func(q *query.Query) { - q.Params["metadata"] = c.QueryMap("metadata") - }, + query.Metadata(c.QueryMap("metadata")), ) if err != nil { ResponseError(c, err) @@ -62,7 +55,9 @@ func (ctl *AccountController) GetAccounts(c *gin.Context) { func (ctl *AccountController) GetAccount(c *gin.Context) { l, _ := c.Get("ledger") - acc, err := l.(*ledger.Ledger).GetAccount(c.Request.Context(), c.Param("address")) + acc, err := l.(*ledger.Ledger).GetAccount( + c.Request.Context(), + c.Param("address")) if err != nil { ResponseError(c, err) return @@ -75,7 +70,8 @@ func (ctl *AccountController) PostAccountMetadata(c *gin.Context) { l, _ := c.Get("ledger") var m core.Metadata - if err := c.Bind(&m); err != nil { + if err := c.ShouldBindJSON(&m); err != nil { + ResponseError(c, err) return } diff --git a/pkg/api/controllers/account_controller_test.go b/pkg/api/controllers/account_controller_test.go index 7390da7848..79db565ad6 100644 --- a/pkg/api/controllers/account_controller_test.go +++ b/pkg/api/controllers/account_controller_test.go @@ -11,6 +11,7 @@ import ( "github.com/numary/ledger/pkg/api/internal" "github.com/numary/ledger/pkg/core" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/fx" ) @@ -28,7 +29,7 @@ func TestGetAccounts(t *testing.T) { }, }, }) - assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) rsp = internal.PostTransaction(t, h, core.TransactionData{ Postings: core.Postings{ @@ -40,7 +41,7 @@ func TestGetAccounts(t *testing.T) { }, }, }) - assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) rsp = internal.PostAccountMetadata(t, h, "bob", core.Metadata{ "roles": json.RawMessage(`"admin"`), @@ -48,58 +49,101 @@ func TestGetAccounts(t *testing.T) { "enabled": json.RawMessage(`"true"`), "a": json.RawMessage(`{"nested": {"key": "hello"}}`), }) - assert.Equal(t, http.StatusNoContent, rsp.Result().StatusCode) + require.Equal(t, http.StatusNoContent, rsp.Result().StatusCode) rsp = internal.CountAccounts(h, url.Values{}) - assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - assert.Equal(t, "3", rsp.Header().Get("Count")) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + require.Equal(t, "3", rsp.Header().Get("Count")) + + type GetAccountsCursor struct { + PageSize int `json:"page_size,omitempty"` + HasMore bool `json:"has_more"` + Previous string `json:"previous,omitempty"` + Next string `json:"next,omitempty"` + Data []core.Account `json:"data"` + } + + type getAccountsResponse struct { + Cursor *GetAccountsCursor `json:"cursor,omitempty"` + } rsp = internal.GetAccounts(h, url.Values{}) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - cursor := internal.DecodeCursorResponse(t, rsp.Body, core.Account{}) - assert.Len(t, cursor.Data, 3) + resp := getAccountsResponse{} + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 3 accounts: world, bob, alice + assert.Len(t, resp.Cursor.Data, 3) + assert.Equal(t, resp.Cursor.Data[0].Address, "world") + assert.Equal(t, resp.Cursor.Data[1].Address, "bob") + assert.Equal(t, resp.Cursor.Data[2].Address, "alice") rsp = internal.GetAccounts(h, url.Values{ "metadata[roles]": []string{"admin"}, }) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - cursor = internal.DecodeCursorResponse(t, rsp.Body, core.Account{}) - assert.Len(t, cursor.Data, 1) + resp = getAccountsResponse{} + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 1 accounts: bob + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].Address, "bob") rsp = internal.GetAccounts(h, url.Values{ "metadata[accountId]": []string{"3"}, }) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - cursor = internal.DecodeCursorResponse(t, rsp.Body, core.Account{}) - assert.Len(t, cursor.Data, 1) + resp = getAccountsResponse{} + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 1 accounts: bob + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].Address, "bob") rsp = internal.GetAccounts(h, url.Values{ "metadata[enabled]": []string{"true"}, }) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - cursor = internal.DecodeCursorResponse(t, rsp.Body, core.Account{}) - assert.Len(t, cursor.Data, 1) + resp = getAccountsResponse{} + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 1 accounts: bob + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].Address, "bob") rsp = internal.GetAccounts(h, url.Values{ "metadata[a.nested.key]": []string{"hello"}, }) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - cursor = internal.DecodeCursorResponse(t, rsp.Body, core.Account{}) - assert.Len(t, cursor.Data, 1) + resp = getAccountsResponse{} + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 1 accounts: bob + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].Address, "bob") rsp = internal.GetAccounts(h, url.Values{ "metadata[unknown]": []string{"key"}, }) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - cursor = internal.DecodeCursorResponse(t, rsp.Body, core.Account{}) + cursor := internal.DecodeCursorResponse(t, rsp.Body, core.Account{}) assert.Len(t, cursor.Data, 0) + rsp = internal.GetAccounts(h, url.Values{ + "after": []string{"bob"}, + }) + assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + resp = getAccountsResponse{} + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 1 accounts: alice + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].Address, "alice") + + rsp = internal.GetAccounts(h, url.Values{ + "address": []string{"b.b"}, + }) + assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + resp = getAccountsResponse{} + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 1 accounts: bob + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].Address, "bob") + return nil }, }) @@ -120,18 +164,17 @@ func TestGetAccount(t *testing.T) { }, }, }) - assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) rsp = internal.PostAccountMetadata(t, h, "alice", core.Metadata{ "foo": json.RawMessage(`"bar"`), }) - assert.Equal(t, http.StatusNoContent, rsp.Result().StatusCode) + require.Equal(t, http.StatusNoContent, rsp.Result().StatusCode) rsp = internal.GetAccount(h, "alice") assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - act := core.Account{} - internal.DecodeSingleResponse(t, rsp.Body, &act) + resp := core.Account{} + internal.DecodeSingleResponse(t, rsp.Body, &resp) assert.EqualValues(t, core.Account{ Address: "alice", @@ -148,7 +191,7 @@ func TestGetAccount(t *testing.T) { Metadata: core.Metadata{ "foo": json.RawMessage(`"bar"`), }, - }, act) + }, resp) return nil }, diff --git a/pkg/api/controllers/base_controller.go b/pkg/api/controllers/base_controller.go index 68a53eb773..ed1f0044ab 100644 --- a/pkg/api/controllers/base_controller.go +++ b/pkg/api/controllers/base_controller.go @@ -12,7 +12,6 @@ import ( "github.com/pkg/errors" ) -// BaseController - type BaseController struct{} func (ctl *BaseController) response(c *gin.Context, status int, data interface{}) { diff --git a/pkg/api/controllers/config_controller.go b/pkg/api/controllers/config_controller.go index 5834f1b20f..59446dc3e4 100644 --- a/pkg/api/controllers/config_controller.go +++ b/pkg/api/controllers/config_controller.go @@ -11,32 +11,27 @@ import ( "gopkg.in/yaml.v3" ) -// ConfigInfo struct type ConfigInfo struct { Server string `json:"server"` Version interface{} `json:"version"` Config *Config `json:"config"` } -// Config struct type Config struct { LedgerStorage *LedgerStorage `json:"storage"` } -// LedgerStorage struct type LedgerStorage struct { Driver string `json:"driver"` Ledgers []string `json:"ledgers"` } -// ConfigController - type ConfigController struct { BaseController Version string StorageDriver storage.Driver } -// NewConfigController - func NewConfigController(version string, storageDriver storage.Driver) ConfigController { return ConfigController{ Version: version, diff --git a/pkg/api/controllers/ledger_controller.go b/pkg/api/controllers/ledger_controller.go index 2e72e697fd..82df04e230 100644 --- a/pkg/api/controllers/ledger_controller.go +++ b/pkg/api/controllers/ledger_controller.go @@ -7,12 +7,10 @@ import ( "github.com/numary/ledger/pkg/ledger" ) -// LedgerController - type LedgerController struct { BaseController } -// NewLedgerController - func NewLedgerController() LedgerController { return LedgerController{} } diff --git a/pkg/api/controllers/script_controller.go b/pkg/api/controllers/script_controller.go index a9fc545734..e5997a7bc4 100644 --- a/pkg/api/controllers/script_controller.go +++ b/pkg/api/controllers/script_controller.go @@ -32,12 +32,10 @@ func EncodeLink(errStr string) string { return fmt.Sprintf("https://play.numscript.org/?payload=%v", payloadB64) } -// ScriptController - type ScriptController struct { BaseController } -// NewScriptController - func NewScriptController() ScriptController { return ScriptController{} } @@ -46,7 +44,7 @@ func (ctl *ScriptController) PostScript(c *gin.Context) { l, _ := c.Get("ledger") var script core.Script - if err := c.ShouldBind(&script); err != nil { + if err := c.ShouldBindJSON(&script); err != nil { panic(err) } diff --git a/pkg/api/controllers/script_controller_test.go b/pkg/api/controllers/script_controller_test.go index 9542adb6ad..56b5b9c9d3 100644 --- a/pkg/api/controllers/script_controller_test.go +++ b/pkg/api/controllers/script_controller_test.go @@ -21,7 +21,6 @@ import ( ) func TestScriptController(t *testing.T) { - type testCase struct { name string script string @@ -83,7 +82,6 @@ send [COIN 100] ( } func TestScriptControllerPreview(t *testing.T) { - internal.RunTest(t, fx.Invoke(func(lc fx.Lifecycle, h *api.API, driver storage.Driver) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { @@ -109,7 +107,7 @@ func TestScriptControllerPreview(t *testing.T) { store, _, err := driver.GetStore(context.Background(), l, true) assert.NoError(t, err) - cursor, err := store.FindTransactions(context.Background(), query.Query{}) + cursor, err := store.GetTransactions(context.Background(), query.Query{}) assert.NoError(t, err) assert.Len(t, cursor.Data, 0) return nil diff --git a/pkg/api/controllers/swagger.yaml b/pkg/api/controllers/swagger.yaml index 682df10036..e087d7c8c5 100644 --- a/pkg/api/controllers/swagger.yaml +++ b/pkg/api/controllers/swagger.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.1 +openapi: 3.0.3 info: title: Ledger API contact: {} @@ -9,8 +9,7 @@ paths: get: tags: - server - summary: Server Info - description: Show server informations + summary: Show server information. operationId: getInfo responses: "200": @@ -19,40 +18,36 @@ paths: application/json: schema: $ref: '#/components/schemas/ConfigInfoResponse' + /{ledger}/accounts: head: - summary: Count accounts + summary: Count the accounts from a ledger. operationId: countAccounts tags: - accounts parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string - - name: after - in: query - description: pagination cursor, will return accounts after given address (in descending order) - schema: - type: string + example: ledger001 - name: address in: query - description: account address - required: false + description: Filter accounts by address pattern (regular expression placed between ^ and $). schema: type: string + example: users:.+ - name: metadata in: query - description: metadata - required: false + description: Filter accounts by metadata key value pairs. Nested objects can be used as seen in the example below. style: deepObject + explode: true schema: type: object - additionalProperties: - type: string properties: {} + example: metadata[key]=value1&metadata[a.nested.key]=value2 responses: "200": description: OK @@ -61,90 +56,116 @@ paths: schema: type: integer get: - summary: List all accounts + summary: List accounts from a ledger. + description: List accounts from a ledger, sorted by address in descending order. operationId: listAccounts tags: - accounts parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 - name: after in: query - description: pagination cursor, will return accounts after given address (in descending order) + description: Pagination cursor, will return accounts after given address, in descending order. schema: type: string + example: users:003 - name: address in: query - description: account address - required: false + description: Filter accounts by address pattern (regular expression placed between ^ and $). schema: type: string + example: users:.+ - name: metadata in: query - description: account address - required: false + description: Filter accounts by metadata key value pairs. Nested objects can be used as seen in the example below. style: deepObject + explode: true schema: type: object - additionalProperties: - type: string - properties: { } + properties: {} + example: metadata[key]=value1&metadata[a.nested.key]=value2 responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/AccountCursorResponse' - /{ledger}/accounts/{accountId}: + required: + - cursor + properties: + cursor: + allOf: + - $ref: '#/components/schemas/Cursor' + - properties: + data: + items: + $ref: '#/components/schemas/Account' + type: array + type: object + required: + - data + + /{ledger}/accounts/{address}: get: - summary: Get account by address + summary: Get account by its address. operationId: getAccount tags: - accounts parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string - - name: accountId + example: ledger001 + - name: address in: path - description: accountId + description: Exact address of the account. required: true schema: type: string + example: users:001 responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/AccountResponse' - /{ledger}/accounts/{accountId}/metadata: + properties: + data: + $ref: '#/components/schemas/AccountWithVolumesAndBalances' + type: object + required: + - data + + /{ledger}/accounts/{address}/metadata: post: - summary: Add metadata to account + summary: Add metadata to an account. operationId: addMetadataToAccount tags: - accounts parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string - - name: accountId + example: ledger001 + - name: address in: path - description: accountId + description: Exact address of the account. required: true schema: type: string + example: users:001 requestBody: description: metadata content: @@ -158,20 +179,21 @@ paths: "400": description: "" content: {} + /{ledger}/mapping: get: tags: - mapping operationId: getMapping - summary: Get mapping - description: Get ledger mapping + summary: Get the mapping of a ledger. parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 responses: "200": description: OK @@ -183,17 +205,16 @@ paths: tags: - mapping operationId: updateMapping - summary: Put mapping - description: Update ledger mapping + summary: Update the mapping of a ledger. parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 requestBody: - description: mapping content: application/json: schema: @@ -212,22 +233,22 @@ paths: tags: - script operationId: runScript - summary: Execute Numscript - description: Execute a Numscript and create the transaction if any + summary: Execute a Numscript. parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 - name: preview in: query - description: Preview mode + description: Set the preview mode. Preview mode doesn't add the logs to the database or publish a message to the message broker. schema: type: boolean + example: true requestBody: - description: script content: application/json: schema: @@ -253,10 +274,11 @@ paths: parameters: - name: ledger in: path - description: ledger + description: name of the ledger required: true schema: type: string + example: ledger001 responses: "200": description: OK @@ -264,47 +286,46 @@ paths: application/json: schema: $ref: '#/components/schemas/StatsResponse' + /{ledger}/transactions: head: tags: - transactions - summary: Count transactions - description: Count transactions mathing given criteria + summary: Count the transactions from a ledger. operationId: countTransactions parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string - - name: after - in: query - description: pagination cursor, will return transactions after given txid - (in descending order) - schema: - type: string + example: ledger001 - name: reference in: query - description: find transactions by reference field + description: Filter transactions by reference field. schema: type: string + example: ref:001 - name: account in: query - description: find transactions with postings involving given account, either - as source or destination + description: Filter transactions with postings involving given account, either + as source or destination. schema: type: string + example: users:001 - name: source in: query - description: find transactions with postings involving given account at source + description: Filter transactions with postings involving given account at source. schema: type: string + example: users:001 - name: destination in: query - description: find transactions with postings involving given account at destination + description: Filter transactions with postings involving given account at destination. schema: type: string + example: users:001 responses: "200": description: OK @@ -315,73 +336,90 @@ paths: get: tags: - transactions - summary: Get all Transactions - description: Get all ledger transactions + summary: List transactions from a ledger. + description: List transactions from a ledger, sorted by txid in descending order. operationId: listTransactions parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 - name: after in: query - description: pagination cursor, will return transactions after given txid - (in descending order) + description: Pagination cursor, will return transactions after given txid + (in descending order). schema: type: string + example: 1234 - name: reference in: query - description: find transactions by reference field + description: Find transactions by reference field. schema: type: string + example: ref:001 - name: account in: query - description: find transactions with postings involving given account, either - as source or destination + description: Find transactions with postings involving given account, either + as source or destination. schema: type: string + example: users:001 - name: source in: query - description: find transactions with postings involving given account at source + description: Find transactions with postings involving given account at source. schema: type: string + example: users:001 - name: destination in: query - description: find transactions with postings involving given account at destination + description: Find transactions with postings involving given account at destination. schema: type: string + example: users:001 responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/TransactionCursorResponse' + type: object + required: + - cursor + properties: + cursor: + allOf: + - $ref: '#/components/schemas/Cursor' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Transaction' + required: + - data post: tags: - transactions - summary: Create Transaction + summary: Create a new transaction to a ledger. operationId: createTransaction - description: |- - Create a new ledger transaction - Commit a new transaction to the ledger parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 - name: preview in: query - description: Preview mode - required: false + description: Set the preview mode. Preview mode doesn't add the logs to the database or publish a message to the message broker. schema: type: boolean + example: true requestBody: - description: transaction content: application/json: schema: @@ -417,22 +455,23 @@ paths: get: tags: - transactions - summary: Get Transaction - description: Get transaction by transaction id + summary: Get transaction from a ledger by its ID. operationId: getTransaction parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 - name: txid in: path - description: txid + description: Transaction ID. required: true schema: type: integer + example: 1234 responses: "200": description: OK @@ -446,26 +485,28 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /{ledger}/transactions/{txid}/metadata: post: tags: - transactions - summary: Set Transaction Metadata + summary: Set the metadata of a transaction by its ID. operationId: addMetadataOnTransaction - description: Set a new metadata to a ledger transaction by transaction id parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 - name: txid in: path - description: txid + description: Transaction ID. required: true schema: type: integer + example: 1234 requestBody: description: metadata content: @@ -476,26 +517,28 @@ paths: "204": description: Empty response content: {} + /{ledger}/transactions/{txid}/revert: post: tags: - transactions operationId: revertTransaction - summary: Revert Transaction - description: Revert a ledger transaction by transaction id + summary: Revert a ledger transaction by its ID. parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 - name: txid in: path - description: txid + description: Transaction ID. required: true schema: type: integer + example: 1234 responses: "200": description: OK @@ -503,24 +546,22 @@ paths: application/json: schema: $ref: '#/components/schemas/TransactionResponse' + /{ledger}/transactions/batch: post: tags: - transactions - summary: Create Transactions Batch + summary: Create a new batch of transactions to a ledger. operationId: CreateTransactions - description: |- - Create a new ledger transactions batch - Commit a batch of new transactions to the ledger parameters: - name: ledger in: path - description: ledger + description: Name of the ledger. required: true schema: type: string + example: ledger001 requestBody: - description: transactions content: application/json: schema: @@ -532,7 +573,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TransactionListResponse' + allOf: + - type: object + properties: + cursor: + $ref: '#/components/schemas/Cursor' + - properties: + data: + items: + $ref: '#/components/schemas/Transaction' + type: array + type: object + required: + - data "400": description: Commit error content: @@ -540,7 +593,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' "409": - description: Confict + description: Conflict content: application/json: schema: @@ -566,6 +619,7 @@ components: type: object nullable: true additionalProperties: {} + example: { admin: true, a: { nested: { key: value}} } ConfigInfo: type: object properties: @@ -591,11 +645,6 @@ components: required: - driver - ledgers - CursorResponse: - type: object - properties: - cursor: - $ref: '#/components/schemas/Cursor' ScriptResult: type: object properties: @@ -620,29 +669,47 @@ components: address: type: string example: users:001 - balances: - type: object - additionalProperties: - type: integer - example: - COIN: 100 + type: + type: string + example: virtual metadata: type: object properties: {} + example: { admin: true, a: { nested: { key: value}} } + AccountWithVolumesAndBalances: + type: object + required: + - address + properties: + address: + type: string + example: users:001 type: type: string example: virtual + metadata: + type: object + properties: {} + example: { admin: true, a: { nested: { key: value}} } volumes: type: object additionalProperties: type: object additionalProperties: type: integer + example: { COIN: { input: 100, output: 0 } } + balances: + type: object + additionalProperties: + type: integer + example: + COIN: 100 Contract: type: object properties: account: type: string + example: users:001 expr: type: object required: @@ -663,12 +730,16 @@ components: properties: amount: type: integer + example: 100 asset: type: string + example: COIN destination: type: string + example: users:002 source: type: string + example: users:001 required: - amount - asset @@ -679,22 +750,29 @@ components: properties: plain: type: string + example: "vars {\naccount $user\n}\nsend [COIN 10] (\n\tsource = @world\n\tdestination = $user\n)\n" vars: type: object properties: {} + example: { + "vars": { + "user": "users:042" + } + } required: - plain Transaction: type: object properties: - metadata: - $ref: '#/components/schemas/Metadata' postings: type: array items: $ref: '#/components/schemas/Posting' reference: type: string + example: ref:001 + metadata: + $ref: '#/components/schemas/Metadata' timestamp: type: string format: date-time @@ -707,15 +785,15 @@ components: TransactionData: type: object properties: - metadata: - type: object - properties: {} postings: type: array items: $ref: '#/components/schemas/Posting' reference: type: string + example: ref:001 + metadata: + $ref: '#/components/schemas/Metadata' required: - postings Transactions: @@ -740,69 +818,15 @@ components: Cursor: type: object required: - - has_more - page_size - - remaining_results - - total + - has_more properties: - has_more: - type: boolean - next: - type: string page_size: type: integer - previous: - type: string - remaining_results: - type: integer - total: - type: integer - AccountCursor: - allOf: - - $ref: '#/components/schemas/Cursor' - - properties: - data: - items: - $ref: '#/components/schemas/Account' - type: array - type: object - required: - - data - AccountCursorResponse: - required: - - cursor - properties: - cursor: - $ref: '#/components/schemas/AccountCursor' - TransactionCursor: - allOf: - - $ref: '#/components/schemas/Cursor' - - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/Transaction' - required: - - data - TransactionCursorResponse: - type: object - required: - - cursor - properties: - cursor: - $ref: '#/components/schemas/TransactionCursor' - TransactionListResponse: - allOf: - - $ref: '#/components/schemas/CursorResponse' - - properties: - data: - items: - $ref: '#/components/schemas/Transaction' - type: array - type: object - required: - - data + example: 1 + has_more: + type: boolean + example: false CreateTransactionResponse: type: object properties: @@ -812,13 +836,6 @@ components: type: array required: - data - AccountResponse: - properties: - data: - $ref: '#/components/schemas/Account' - type: object - required: - - data TransactionResponse: properties: data: @@ -862,3 +879,4 @@ components: $ref: '#/components/schemas/ErrorCode' error_message: type: string + example: internal error diff --git a/pkg/api/controllers/transaction_controller.go b/pkg/api/controllers/transaction_controller.go index 16ec88ea9f..2fcbefe743 100644 --- a/pkg/api/controllers/transaction_controller.go +++ b/pkg/api/controllers/transaction_controller.go @@ -12,12 +12,10 @@ import ( "github.com/numary/ledger/pkg/ledger/query" ) -// TransactionController - type TransactionController struct { BaseController } -// NewTransactionController - func NewTransactionController() TransactionController { return TransactionController{} } @@ -27,9 +25,10 @@ func (ctl *TransactionController) CountTransactions(c *gin.Context) { count, err := l.(*ledger.Ledger).CountTransactions( c.Request.Context(), - query.After(c.Query("after")), query.Reference(c.Query("reference")), query.Account(c.Query("account")), + query.Source(c.Query("source")), + query.Destination(c.Query("destination")), ) if err != nil { ResponseError(c, err) @@ -41,7 +40,7 @@ func (ctl *TransactionController) CountTransactions(c *gin.Context) { func (ctl *TransactionController) GetTransactions(c *gin.Context) { l, _ := c.Get("ledger") - cursor, err := l.(*ledger.Ledger).FindTransactions( + cursor, err := l.(*ledger.Ledger).GetTransactions( c.Request.Context(), query.After(c.Query("after")), query.Reference(c.Query("reference")), @@ -63,7 +62,7 @@ func (ctl *TransactionController) PostTransaction(c *gin.Context) { preview := ok && (strings.ToUpper(value) == "YES" || strings.ToUpper(value) == "TRUE" || value == "1") var t core.TransactionData - if err := c.ShouldBind(&t); err != nil { + if err := c.ShouldBindJSON(&t); err != nil { panic(err) } @@ -77,10 +76,12 @@ func (ctl *TransactionController) PostTransaction(c *gin.Context) { ResponseError(c, err) return } + status := http.StatusOK if preview { status = http.StatusNotModified } + ctl.response(c, status, result) } @@ -126,7 +127,7 @@ func (ctl *TransactionController) PostTransactionMetadata(c *gin.Context) { l, _ := c.Get("ledger") var m core.Metadata - if err := c.ShouldBind(&m); err != nil { + if err := c.ShouldBindJSON(&m); err != nil { panic(err) } @@ -147,13 +148,13 @@ func (ctl *TransactionController) PostTransactionMetadata(c *gin.Context) { func (ctl *TransactionController) PostTransactionsBatch(c *gin.Context) { l, _ := c.Get("ledger") - var transactions core.Transactions - if err := c.ShouldBindJSON(&transactions); err != nil { + var t core.Transactions + if err := c.ShouldBindJSON(&t); err != nil { ResponseError(c, err) return } - _, txs, err := l.(*ledger.Ledger).Commit(c.Request.Context(), transactions.Transactions) + _, txs, err := l.(*ledger.Ledger).Commit(c.Request.Context(), t.Transactions) if err != nil { ResponseError(c, err) return diff --git a/pkg/api/controllers/transaction_controller_test.go b/pkg/api/controllers/transaction_controller_test.go index 36e19be206..11966bc16a 100644 --- a/pkg/api/controllers/transaction_controller_test.go +++ b/pkg/api/controllers/transaction_controller_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "net/http" + "net/url" "os" "testing" @@ -17,17 +18,18 @@ import ( "github.com/numary/ledger/pkg/storage" "github.com/numary/ledger/pkg/storage/sqlstorage" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/fx" ) func TestCommitTransaction(t *testing.T) { - type testCase struct { name string transactions []core.TransactionData expectedStatusCode int expectedErrorCode string } + testCases := []testCase{ { name: "nominal", @@ -264,8 +266,9 @@ func TestGetTransactions(t *testing.T) { Asset: "USD", }, }, + Reference: "ref:001", }) - assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) rsp = internal.PostTransaction(t, api, core.TransactionData{ Postings: core.Postings{ @@ -279,20 +282,59 @@ func TestGetTransactions(t *testing.T) { Metadata: map[string]json.RawMessage{ "foo": json.RawMessage(`"bar"`), }, + Reference: "ref:002", }) - assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + + rsp = internal.CountTransactions(api, url.Values{}) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + require.Equal(t, "2", rsp.Header().Get("Count")) + + type GetTransactionsCursor struct { + PageSize int `json:"page_size,omitempty"` + HasMore bool `json:"has_more"` + Previous string `json:"previous,omitempty"` + Next string `json:"next,omitempty"` + Data []core.Transaction `json:"data"` + } - rsp = internal.CountTransactions(api) + type getTransactionsResponse struct { + Cursor *GetTransactionsCursor `json:"cursor,omitempty"` + } + + rsp = internal.GetTransactions(api, url.Values{}) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) - assert.Equal(t, "2", rsp.Header().Get("Count")) + resp := getTransactionsResponse{} + + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + // 2 transactions: txid 1 and txid 0 + assert.Len(t, resp.Cursor.Data, 2) + assert.Equal(t, resp.Cursor.Data[0].ID, uint64(1)) + assert.Equal(t, resp.Cursor.Data[1].ID, uint64(0)) + assert.False(t, resp.Cursor.HasMore) - rsp = internal.GetTransactions(api) + rsp = internal.GetTransactions(api, url.Values{ + "after": []string{"1"}, + }) assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + resp = getTransactionsResponse{} + // 1 transaction: txid 0 + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].ID, uint64(0)) + assert.False(t, resp.Cursor.HasMore) - cursor := internal.DecodeCursorResponse(t, rsp.Body, core.Transaction{}) + rsp = internal.GetTransactions(api, url.Values{ + "reference": []string{"ref:001"}, + }) + assert.Equal(t, http.StatusOK, rsp.Result().StatusCode) + resp = getTransactionsResponse{} + // 1 transaction: txid 0 + assert.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &resp)) + assert.Len(t, resp.Cursor.Data, 1) + assert.Equal(t, resp.Cursor.Data[0].ID, uint64(0)) + assert.False(t, resp.Cursor.HasMore) - assert.Len(t, cursor.Data, 2) - assert.False(t, cursor.HasMore) return nil }, }) @@ -377,7 +419,7 @@ func TestTooManyClient(t *testing.T) { }(tx) } - rsp := internal.GetTransactions(api) + rsp := internal.GetTransactions(api, url.Values{}) assert.Equal(t, http.StatusServiceUnavailable, rsp.Result().StatusCode) return nil }, diff --git a/pkg/api/internal/testing.go b/pkg/api/internal/testing.go index 0f6e8808bb..61122aa87d 100644 --- a/pkg/api/internal/testing.go +++ b/pkg/api/internal/testing.go @@ -106,14 +106,16 @@ func PostTransactionMetadata(t *testing.T, handler http.Handler, id uint64, m co return rec } -func CountTransactions(handler http.Handler) *httptest.ResponseRecorder { +func CountTransactions(handler http.Handler, query url.Values) *httptest.ResponseRecorder { req, rec := NewRequest(http.MethodHead, "/"+testingLedger+"/transactions", nil) + req.URL.RawQuery = query.Encode() handler.ServeHTTP(rec, req) return rec } -func GetTransactions(handler http.Handler) *httptest.ResponseRecorder { +func GetTransactions(handler http.Handler, query url.Values) *httptest.ResponseRecorder { req, rec := NewRequest(http.MethodGet, "/"+testingLedger+"/transactions", nil) + req.URL.RawQuery = query.Encode() handler.ServeHTTP(rec, req) return rec } diff --git a/pkg/api/middlewares/ledger_middleware.go b/pkg/api/middlewares/ledger_middleware.go index 4a6ac7ce2c..17ad5cb3fc 100644 --- a/pkg/api/middlewares/ledger_middleware.go +++ b/pkg/api/middlewares/ledger_middleware.go @@ -7,12 +7,10 @@ import ( "github.com/numary/ledger/pkg/ledger" ) -// LedgerMiddleware struct type LedgerMiddleware struct { resolver *ledger.Resolver } -// NewLedgerMiddleware - func NewLedgerMiddleware( resolver *ledger.Resolver, ) LedgerMiddleware { @@ -21,7 +19,6 @@ func NewLedgerMiddleware( } } -// LedgerMiddleware - func (m *LedgerMiddleware) LedgerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { name := c.Param("ledger") diff --git a/pkg/api/routes/routes.go b/pkg/api/routes/routes.go index d90afa69dc..687cda94e9 100644 --- a/pkg/api/routes/routes.go +++ b/pkg/api/routes/routes.go @@ -56,7 +56,6 @@ var AllScopes = []string{ ScopesStatsRead, } -// Routes - type Routes struct { resolver *ledger.Resolver ledgerMiddleware middlewares.LedgerMiddleware @@ -72,7 +71,6 @@ type Routes struct { useScopes UseScopes } -// NewRoutes - func NewRoutes( globalMiddlewares []gin.HandlerFunc, perLedgerMiddlewares []gin.HandlerFunc, @@ -119,18 +117,15 @@ func (r *Routes) wrapWithScopes(handler gin.HandlerFunc, scopes ...string) gin.H } } -// Engine - func (r *Routes) Engine() *gin.Engine { engine := gin.New() - // Default Middlewares engine.Use(r.globalMiddlewares...) engine.GET("/_health", r.healthController.Check) engine.GET("/swagger.yaml", r.configController.GetDocsAsYaml) engine.GET("/swagger.json", r.configController.GetDocsAsJSON) - // API Routes engine.GET("/_info", r.configController.GetInfo) router := engine.Group("/:ledger", append(r.perLedgerMiddlewares, r.ledgerMiddleware.LedgerMiddleware())...) @@ -138,6 +133,12 @@ func (r *Routes) Engine() *gin.Engine { // LedgerController router.GET("/stats", r.wrapWithScopes(r.ledgerController.GetStats, ScopesStatsRead)) + // AccountController + router.GET("/accounts", r.wrapWithScopes(r.accountController.GetAccounts, ScopeAccountsRead, ScopeAccountsWrite)) + router.HEAD("/accounts", r.wrapWithScopes(r.accountController.CountAccounts, ScopeAccountsRead, ScopeAccountsWrite)) + router.GET("/accounts/:address", r.wrapWithScopes(r.accountController.GetAccount, ScopeAccountsRead, ScopeAccountsWrite)) + router.POST("/accounts/:address/metadata", r.wrapWithScopes(r.accountController.PostAccountMetadata, ScopeAccountsWrite)) + // TransactionController router.GET("/transactions", r.wrapWithScopes(r.transactionController.GetTransactions, ScopeTransactionsRead, ScopeTransactionsWrite)) router.HEAD("/transactions", r.wrapWithScopes(r.transactionController.CountTransactions, ScopeTransactionsRead, ScopeTransactionsWrite)) @@ -147,12 +148,6 @@ func (r *Routes) Engine() *gin.Engine { router.POST("/transactions/:txid/revert", r.wrapWithScopes(r.transactionController.RevertTransaction, ScopeTransactionsWrite)) router.POST("/transactions/:txid/metadata", r.wrapWithScopes(r.transactionController.PostTransactionMetadata, ScopeTransactionsWrite)) - // AccountController - router.GET("/accounts", r.wrapWithScopes(r.accountController.GetAccounts, ScopeAccountsRead, ScopeAccountsWrite)) - router.HEAD("/accounts", r.wrapWithScopes(r.accountController.CountAccounts, ScopeAccountsRead, ScopeAccountsWrite)) - router.GET("/accounts/:address", r.wrapWithScopes(r.accountController.GetAccount, ScopeAccountsRead, ScopeAccountsWrite)) - router.POST("/accounts/:address/metadata", r.wrapWithScopes(r.accountController.PostAccountMetadata, ScopeAccountsWrite)) - // MappingController router.GET("/mapping", r.wrapWithScopes(r.mappingController.GetMapping, ScopeMappingRead, ScopeMappingWrite)) router.PUT("/mapping", r.wrapWithScopes(r.mappingController.PutMapping, ScopeMappingWrite)) diff --git a/pkg/core/transaction.go b/pkg/core/transaction.go index f42c7a8d59..3332064063 100644 --- a/pkg/core/transaction.go +++ b/pkg/core/transaction.go @@ -7,7 +7,6 @@ import ( json "github.com/gibson042/canonicaljson-go" ) -// Transactions struct type Transactions struct { Transactions []TransactionData `json:"transactions" binding:"required,dive"` } diff --git a/pkg/ledger/executor_test.go b/pkg/ledger/executor_test.go index 49b4efcfda..b11699798b 100644 --- a/pkg/ledger/executor_test.go +++ b/pkg/ledger/executor_test.go @@ -338,10 +338,10 @@ func TestSetTxMeta(t *testing.T) { assertBalance(t, l, "user:042", "COIN", 10) - tx, err := l.GetLastTransaction(context.Background()) + last, err := l.store.GetLastTransaction(context.Background()) require.NoError(t, err) - value, err := machine.NewValueFromTypedJSON(tx.Metadata["test_meta"]) + value, err := machine.NewValueFromTypedJSON(last.Metadata["test_meta"]) require.NoError(t, err) assert.True(t, machine.ValueEquals(*value, machine.Monetary{ diff --git a/pkg/ledger/ledger.go b/pkg/ledger/ledger.go index 3744d7b3b5..5186938b46 100644 --- a/pkg/ledger/ledger.go +++ b/pkg/ledger/ledger.go @@ -45,41 +45,25 @@ func NewLedger(name string, store storage.Store, locker Locker, monitor Monitor) } func (l *Ledger) Close(ctx context.Context) error { - err := l.store.Close(ctx) - if err != nil { + if err := l.store.Close(ctx); err != nil { return errors.Wrap(err, "closing store") } return nil } func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (core.AggregatedVolumes, []core.Transaction, []core.Log, error) { - - timestamp := time.Now().UTC() - mapping, err := l.store.LoadMapping(ctx) if err != nil { return nil, nil, nil, errors.Wrap(err, "loading mapping") } - contracts := make([]core.Contract, 0) - if mapping != nil { - contracts = append(contracts, mapping.Contracts...) - } - contracts = append(contracts, DefaultContracts...) - - ret := make([]core.Transaction, 0) - aggregatedVolumes := core.AggregatedVolumes{} - lastLog, err := l.store.LastLog(ctx) if err != nil { return nil, nil, nil, err } - accounts := make(map[string]core.Account, 0) - logs := make([]core.Log, 0) - - nextTxId := uint64(0) - lastTx, err := l.store.LastTransaction(ctx) + var nextTxId uint64 + lastTx, err := l.store.GetLastTransaction(ctx) if err != nil { return nil, nil, nil, err } @@ -87,20 +71,23 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (core nextTxId = lastTx.ID + 1 } - for i := range ts { - tx := core.Transaction{ - TransactionData: ts[i], - ID: nextTxId, - Timestamp: timestamp.Format(time.RFC3339), - } - nextTxId++ + txs := make([]core.Transaction, 0) + aggregatedVolumes := core.AggregatedVolumes{} + accounts := make(map[string]core.Account, 0) + logs := make([]core.Log, 0) + contracts := make([]core.Contract, 0) + if mapping != nil { + contracts = append(contracts, mapping.Contracts...) + } + contracts = append(contracts, DefaultContracts...) - if len(ts[i].Postings) == 0 { + for i, t := range ts { + if len(t.Postings) == 0 { return nil, nil, nil, NewTransactionCommitError(i, NewValidationError("transaction has no postings")) } rf := core.AggregatedVolumes{} - for _, p := range ts[i].Postings { + for _, p := range t.Postings { if p.Amount < 0 { return nil, nil, nil, NewTransactionCommitError(i, NewValidationError("negative amount")) } @@ -133,9 +120,7 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (core } for addr := range rf { - - _, ok := aggregatedVolumes[addr] - if !ok { + if _, ok := aggregatedVolumes[addr]; !ok { aggregatedVolumes[addr], err = l.store.AggregateVolumes(ctx, addr) if err != nil { return nil, nil, nil, err @@ -144,10 +129,7 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (core for asset, volumes := range rf[addr] { if _, ok := aggregatedVolumes[addr][asset]; !ok { - aggregatedVolumes[addr][asset] = map[string]int64{ - "input": 0, - "output": 0, - } + aggregatedVolumes[addr][asset] = map[string]int64{"input": 0, "output": 0} } if addr != "world" { expectedBalance := aggregatedVolumes[addr][asset]["input"] - aggregatedVolumes[addr][asset]["output"] + volumes["input"] - volumes["output"] @@ -162,14 +144,13 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (core accounts[addr] = account } - ok = contract.Expr.Eval(core.EvalContext{ + if ok = contract.Expr.Eval(core.EvalContext{ Variables: map[string]interface{}{ "balance": float64(expectedBalance), }, Metadata: account.Metadata, Asset: asset, - }) - if !ok { + }); !ok { return nil, nil, nil, NewTransactionCommitError(i, NewInsufficientFundError(asset)) } break @@ -180,13 +161,20 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (core aggregatedVolumes[addr][asset]["output"] += volumes["output"] } } - ret = append(ret, tx) + + tx := core.Transaction{ + TransactionData: t, + ID: nextTxId, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + txs = append(txs, tx) newLog := core.NewTransactionLog(lastLog, tx) lastLog = &newLog logs = append(logs, newLog) + nextTxId++ } - return aggregatedVolumes, ret, logs, nil + return aggregatedVolumes, txs, logs, nil } func (l *Ledger) Commit(ctx context.Context, ts []core.TransactionData) (core.AggregatedVolumes, []core.Transaction, error) { @@ -201,8 +189,7 @@ func (l *Ledger) Commit(ctx context.Context, ts []core.TransactionData) (core.Ag return nil, nil, err } - err = l.store.AppendLog(ctx, logs...) - if err != nil { + if err = l.store.AppendLog(ctx, logs...); err != nil { switch { case storage.IsErrorCode(err, storage.ConstraintFailed): return nil, nil, NewConflictError() @@ -223,54 +210,22 @@ func (l *Ledger) CommitPreview(ctx context.Context, ts []core.TransactionData) ( } defer unlock(ctx) - balances, ret, _, err := l.processTx(ctx, ts) - return balances, ret, err -} - -// TODO: This is only used for testing -// I think we should remove this and all related code -// We don't need any testing logic in the business code. -func (l *Ledger) GetLastTransaction(ctx context.Context) (core.Transaction, error) { - var tx core.Transaction - - q := query.New() - q.Modify(query.Limit(1)) - - c, err := l.store.FindTransactions(ctx, q) - - if err != nil { - return tx, err - } - - txs := (c.Data).([]core.Transaction) - - if len(txs) == 0 { - return tx, nil - } - - tx = txs[0] - - return tx, nil + volumes, txs, _, err := l.processTx(ctx, ts) + return volumes, txs, err } -func (l *Ledger) FindTransactions(ctx context.Context, m ...query.Modifier) (sharedapi.Cursor, error) { +func (l *Ledger) GetTransactions(ctx context.Context, m ...query.Modifier) (sharedapi.Cursor, error) { q := query.New(m) - c, err := l.store.FindTransactions(ctx, q) - - return c, err + return l.store.GetTransactions(ctx, q) } func (l *Ledger) CountTransactions(ctx context.Context, m ...query.Modifier) (uint64, error) { q := query.New(m) - c, err := l.store.CountTransactions(ctx, q) - - return c, err + return l.store.CountTransactions(ctx, q) } func (l *Ledger) GetTransaction(ctx context.Context, id uint64) (core.Transaction, error) { - tx, err := l.store.GetTransaction(ctx, id) - - return tx, err + return l.store.GetTransaction(ctx, id) } func (l *Ledger) SaveMapping(ctx context.Context, mapping core.Mapping) error { @@ -323,18 +278,12 @@ func (l *Ledger) RevertTransaction(ctx context.Context, id uint64) (*core.Transa func (l *Ledger) CountAccounts(ctx context.Context, m ...query.Modifier) (uint64, error) { q := query.New(m) - - count, err := l.store.CountAccounts(ctx, q) - - return count, err + return l.store.CountAccounts(ctx, q) } -func (l *Ledger) FindAccounts(ctx context.Context, m ...query.Modifier) (sharedapi.Cursor, error) { +func (l *Ledger) GetAccounts(ctx context.Context, m ...query.Modifier) (sharedapi.Cursor, error) { q := query.New(m) - - c, err := l.store.FindAccounts(ctx, q) - - return c, err + return l.store.GetAccounts(ctx, q) } func (l *Ledger) GetAccount(ctx context.Context, address string) (core.Account, error) { @@ -344,7 +293,6 @@ func (l *Ledger) GetAccount(ctx context.Context, address string) (core.Account, } volumes, err := l.store.AggregateVolumes(ctx, address) - if err != nil { return account, err } diff --git a/pkg/ledger/ledger_test.go b/pkg/ledger/ledger_test.go index 15a775b897..88408ac106 100644 --- a/pkg/ledger/ledger_test.go +++ b/pkg/ledger/ledger_test.go @@ -346,13 +346,6 @@ func TestReference(t *testing.T) { }) } -func TestLast(t *testing.T) { - with(func(l *Ledger) { - _, err := l.GetLastTransaction(context.Background()) - assert.NoError(t, err) - }) -} - func TestAccountMetadata(t *testing.T) { with(func(l *Ledger) { @@ -380,7 +373,7 @@ func TestAccountMetadata(t *testing.T) { } { - // We have to create at least one transaction to retrieve an account from FindAccounts store method + // We have to create at least one transaction to retrieve an account from GetAccounts store method _, _, err := l.Commit(context.Background(), []core.TransactionData{ { Postings: core.Postings{ @@ -395,12 +388,12 @@ func TestAccountMetadata(t *testing.T) { }) assert.NoError(t, err) - cursor, err := l.FindAccounts(context.Background(), query.Account("users:001")) + cursor, err := l.GetAccounts(context.Background(), query.Account("users:001")) assert.NoError(t, err) accounts, ok := cursor.Data.([]core.Account) require.Truef(t, ok, "wrong cursor type: %v", reflect.TypeOf(cursor.Data)) - require.True(t, len(accounts) > 0, "no accounts returned by find") + require.True(t, len(accounts) > 0, "no accounts returned by get accounts") metaFound := false for _, acc := range accounts { @@ -431,7 +424,7 @@ func TestTransactionMetadata(t *testing.T) { }}) require.NoError(t, err) - tx, err := l.GetLastTransaction(context.Background()) + tx, err := l.store.GetLastTransaction(context.Background()) require.NoError(t, err) err = l.SaveMeta(context.Background(), core.MetaTargetTypeTransaction, tx.ID, core.Metadata{ @@ -444,7 +437,7 @@ func TestTransactionMetadata(t *testing.T) { }) require.NoError(t, err) - tx, err = l.GetLastTransaction(context.Background()) + tx, err = l.store.GetLastTransaction(context.Background()) require.NoError(t, err) meta, ok := tx.Metadata["a random metadata"] @@ -474,7 +467,7 @@ func TestSaveTransactionMetadata(t *testing.T) { }}) require.NoError(t, err) - tx, err := l.GetLastTransaction(context.Background()) + tx, err := l.store.GetLastTransaction(context.Background()) require.NoError(t, err) meta, ok := tx.Metadata["a metadata"] @@ -503,23 +496,23 @@ func TestGetTransaction(t *testing.T) { }}) require.NoError(t, err) - last, err := l.GetLastTransaction(context.Background()) + last, err := l.store.GetLastTransaction(context.Background()) require.NoError(t, err) tx, err := l.GetTransaction(context.Background(), last.ID) require.NoError(t, err) - assert.True(t, reflect.DeepEqual(tx, last)) + assert.True(t, reflect.DeepEqual(tx, *last)) }) } -func TestFindTransactions(t *testing.T) { +func TestGetTransactions(t *testing.T) { with(func(l *Ledger) { tx := core.TransactionData{ Postings: []core.Posting{ { Source: "world", - Destination: "test_find_transactions", + Destination: "test_get_transactions", Amount: 100, Asset: "COIN", }, @@ -529,13 +522,13 @@ func TestFindTransactions(t *testing.T) { _, _, err := l.Commit(context.Background(), []core.TransactionData{tx}) require.NoError(t, err) - res, err := l.FindTransactions(context.Background()) + res, err := l.GetTransactions(context.Background()) require.NoError(t, err) txs, ok := res.Data.([]core.Transaction) require.True(t, ok) - assert.Equal(t, "test_find_transactions", txs[0].Postings[0].Destination) + assert.Equal(t, "test_get_transactions", txs[0].Postings[0].Destination) }) } @@ -653,10 +646,10 @@ func BenchmarkGetAccount(b *testing.B) { }) } -func BenchmarkFindTransactions(b *testing.B) { +func BenchmarkGetTransactions(b *testing.B) { with(func(l *Ledger) { for i := 0; i < b.N; i++ { - _, err := l.FindTransactions(context.Background()) + _, err := l.GetTransactions(context.Background()) require.NoError(b, err) } }) diff --git a/pkg/ledger/query/query.go b/pkg/ledger/query/query.go index 069fc4c9e4..6d925fcd53 100644 --- a/pkg/ledger/query/query.go +++ b/pkg/ledger/query/query.go @@ -86,3 +86,9 @@ func Reference(v string) func(*Query) { q.Params["reference"] = v } } + +func Metadata(v map[string]string) func(*Query) { + return func(q *Query) { + q.Params["metadata"] = v + } +} diff --git a/pkg/opentelemetry/opentelemetrytraces/storage.go b/pkg/opentelemetry/opentelemetrytraces/storage.go index 77dd74bff8..0f2062485e 100644 --- a/pkg/opentelemetry/opentelemetrytraces/storage.go +++ b/pkg/opentelemetry/opentelemetrytraces/storage.go @@ -41,13 +41,13 @@ func (o *openTelemetryStorage) handle(ctx context.Context, name string, fn func( return err } -func (o *openTelemetryStorage) LastTransaction(ctx context.Context) (ret *core.Transaction, err error) { - handlingErr := o.handle(ctx, "LastTransaction", func(ctx context.Context) error { - ret, err = o.underlying.LastTransaction(ctx) +func (o *openTelemetryStorage) GetLastTransaction(ctx context.Context) (ret *core.Transaction, err error) { + handlingErr := o.handle(ctx, "GetLastTransaction", func(ctx context.Context) error { + ret, err = o.underlying.GetLastTransaction(ctx) return err }) if handlingErr != nil { - sharedlogging.Errorf("opentelemetry LastTransaction: %s", handlingErr) + sharedlogging.Errorf("opentelemetry GetLastTransaction: %s", handlingErr) } return } @@ -96,13 +96,13 @@ func (o *openTelemetryStorage) CountTransactions(ctx context.Context, q query.Qu return } -func (o *openTelemetryStorage) FindTransactions(ctx context.Context, query query.Query) (q sharedapi.Cursor, err error) { - handlingErr := o.handle(ctx, "FindTransactions", func(ctx context.Context) error { - q, err = o.underlying.FindTransactions(ctx, query) +func (o *openTelemetryStorage) GetTransactions(ctx context.Context, query query.Query) (q sharedapi.Cursor, err error) { + handlingErr := o.handle(ctx, "GetTransactions", func(ctx context.Context) error { + q, err = o.underlying.GetTransactions(ctx, query) return err }) if handlingErr != nil { - sharedlogging.Errorf("opentelemetry FindTransactions: %s", handlingErr) + sharedlogging.Errorf("opentelemetry GetTransactions: %s", handlingErr) } return } @@ -151,13 +151,13 @@ func (o *openTelemetryStorage) CountAccounts(ctx context.Context, q query.Query) return } -func (o *openTelemetryStorage) FindAccounts(ctx context.Context, query query.Query) (q sharedapi.Cursor, err error) { - handlingErr := o.handle(ctx, "FindAccounts", func(ctx context.Context) error { - q, err = o.underlying.FindAccounts(ctx, query) +func (o *openTelemetryStorage) GetAccounts(ctx context.Context, query query.Query) (q sharedapi.Cursor, err error) { + handlingErr := o.handle(ctx, "GetAccounts", func(ctx context.Context) error { + q, err = o.underlying.GetAccounts(ctx, query) return err }) if handlingErr != nil { - sharedlogging.Errorf("opentelemetry FindAccounts: %s", handlingErr) + sharedlogging.Errorf("opentelemetry GetAccounts: %s", handlingErr) } return } diff --git a/pkg/opentelemetry/opentelemetrytraces/storage_test.go b/pkg/opentelemetry/opentelemetrytraces/storage_test.go index 5cd7fd5721..0a74586167 100644 --- a/pkg/opentelemetry/opentelemetrytraces/storage_test.go +++ b/pkg/opentelemetry/opentelemetrytraces/storage_test.go @@ -40,16 +40,16 @@ func TestStore(t *testing.T) { fn: testAggregateVolumes, }, { - name: "FindAccounts", - fn: testFindAccounts, + name: "GetAccounts", + fn: testGetAccounts, }, { name: "CountTransactions", fn: testCountTransactions, }, { - name: "FindTransactions", - fn: testFindTransactions, + name: "GetTransactions", + fn: testGetTransactions, }, { name: "GetTransaction", @@ -93,8 +93,8 @@ func testAggregateVolumes(t *testing.T, store storage.Store) { assert.NoError(t, err) } -func testFindAccounts(t *testing.T, store storage.Store) { - _, err := store.FindAccounts(context.Background(), query.Query{ +func testGetAccounts(t *testing.T, store storage.Store) { + _, err := store.GetAccounts(context.Background(), query.Query{ Limit: 1, }) assert.NoError(t, err) @@ -105,8 +105,8 @@ func testCountTransactions(t *testing.T, store storage.Store) { assert.NoError(t, err) } -func testFindTransactions(t *testing.T, store storage.Store) { - _, err := store.FindTransactions(context.Background(), query.Query{ +func testGetTransactions(t *testing.T, store storage.Store) { + _, err := store.GetTransactions(context.Background(), query.Query{ Limit: 1, }) assert.NoError(t, err) diff --git a/pkg/storage/sqlstorage/accounts.go b/pkg/storage/sqlstorage/accounts.go index 869e91ceef..392f9f28ae 100644 --- a/pkg/storage/sqlstorage/accounts.go +++ b/pkg/storage/sqlstorage/accounts.go @@ -3,6 +3,7 @@ package sqlstorage import ( "context" "database/sql" + "fmt" "math" "strings" @@ -13,18 +14,20 @@ import ( ) func (s *Store) accountsQuery(p map[string]interface{}) *sqlbuilder.SelectBuilder { - sb := sqlbuilder.NewSelectBuilder() - sb. - From(s.schema.Table("accounts")) + sb.From(s.schema.Table("accounts")) if metadata, ok := p["metadata"]; ok { - for k, metaValue := range metadata.(map[string]string) { - arg := sb.Args.Add(metaValue) + for key, value := range metadata.(map[string]string) { + arg := sb.Args.Add(value) // TODO: Need to find another way to specify the prefix since Table() methods does not make sense for functions and procedures - sb.Where(s.schema.Table("meta_compare(metadata, " + arg + ", '" + strings.Join(strings.Split(k, "."), "', '") + "')")) + sb.Where(s.schema.Table( + fmt.Sprintf("%s(metadata, %s, '%s')", + SQLCustomFuncMetaCompare, arg, strings.ReplaceAll(key, ".", "', '")), + )) } } + if address, ok := p["address"]; ok && address.(string) != "" { arg := sb.Args.Add("^" + address.(string) + "$") switch s.Schema().Flavor() { @@ -38,7 +41,7 @@ func (s *Store) accountsQuery(p map[string]interface{}) *sqlbuilder.SelectBuilde return sb } -func (s *Store) findAccounts(ctx context.Context, exec executor, q query.Query) (sharedapi.Cursor, error) { +func (s *Store) getAccounts(ctx context.Context, exec executor, q query.Query) (sharedapi.Cursor, error) { // We fetch an additional account to know if we have more documents q.Limit = int(math.Max(-1, math.Min(float64(q.Limit), 100))) + 1 @@ -55,7 +58,6 @@ func (s *Store) findAccounts(ctx context.Context, exec executor, q query.Query) } sqlq, args := sb.BuildWithFlavor(s.schema.Flavor()) - rows, err := exec.QueryContext(ctx, sqlq, args...) if err != nil { return c, s.error(err) @@ -68,18 +70,10 @@ func (s *Store) findAccounts(ctx context.Context, exec executor, q query.Query) for rows.Next() { account := core.Account{} - var ( - addr sql.NullString - m sql.NullString - ) - err := rows.Scan(&addr, &m) - if err != nil { - return c, err - } - err = rows.Scan(&account.Address, &account.Metadata) - if err != nil { + if err := rows.Scan(&account.Address, &account.Metadata); err != nil { return c, err } + results = append(results, account) } if rows.Err() != nil { @@ -97,20 +91,21 @@ func (s *Store) findAccounts(ctx context.Context, exec executor, q query.Query) return c, nil } -func (s *Store) FindAccounts(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { - return s.findAccounts(ctx, s.schema, q) +func (s *Store) GetAccounts(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { + return s.getAccounts(ctx, s.schema, q) } func (s *Store) getAccount(ctx context.Context, exec executor, addr string) (core.Account, error) { - sb := sqlbuilder.NewSelectBuilder() - sb. - Select("address", "metadata"). + sb.Select("address", "metadata"). From(s.schema.Table("accounts")). Where(sb.Equal("address", addr)) sqlq, args := sb.BuildWithFlavor(s.schema.Flavor()) row := exec.QueryRowContext(ctx, sqlq, args...) + if err := row.Err(); err != nil { + return core.Account{}, err + } account := core.Account{} err := row.Scan(&account.Address, &account.Metadata) diff --git a/pkg/storage/sqlstorage/mapping.go b/pkg/storage/sqlstorage/mapping.go index 883e8818a9..8a9543a17e 100644 --- a/pkg/storage/sqlstorage/mapping.go +++ b/pkg/storage/sqlstorage/mapping.go @@ -13,44 +13,26 @@ import ( const mappingId = "0000" func (s *Store) loadMapping(ctx context.Context, exec executor) (*core.Mapping, error) { - sb := sqlbuilder.NewSelectBuilder() - sb. - Select("mapping"). - From(s.schema.Table("mapping")) + sb.Select("mapping").From(s.schema.Table("mapping")) sqlq, args := sb.BuildWithFlavor(s.schema.Flavor()) - - rows, err := exec.QueryContext(ctx, sqlq, args...) - if err != nil { - return nil, s.error(err) - } - defer func(rows *sql.Rows) { - if err := rows.Close(); err != nil { - panic(err) - } - }(rows) - - if !rows.Next() { - if rows.Err() != nil { - return nil, s.error(rows.Err()) - } - return nil, nil - } + row := exec.QueryRowContext(ctx, sqlq, args...) var mappingString string - err = rows.Scan(&mappingString) - if err != nil { + if err := row.Scan(&mappingString); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } return nil, err } - m := &core.Mapping{} - err = json.Unmarshal([]byte(mappingString), m) - if err != nil { + m := core.Mapping{} + if err := json.Unmarshal([]byte(mappingString), &m); err != nil { return nil, err } - return m, nil + return &m, nil } func (s *Store) LoadMapping(ctx context.Context) (*core.Mapping, error) { @@ -58,7 +40,6 @@ func (s *Store) LoadMapping(ctx context.Context) (*core.Mapping, error) { } func (s *Store) saveMapping(ctx context.Context, exec executor, mapping core.Mapping) error { - data, err := json.Marshal(mapping) if err != nil { return err diff --git a/pkg/storage/sqlstorage/migrations_test.go b/pkg/storage/sqlstorage/migrations_test.go index 944d2de900..567b26b86c 100644 --- a/pkg/storage/sqlstorage/migrations_test.go +++ b/pkg/storage/sqlstorage/migrations_test.go @@ -264,7 +264,7 @@ var postMigrate = map[string]func(t *testing.T, store *sqlstorage.Store){ return } - txs, err := store.FindTransactions(context.Background(), query.Query{ + txs, err := store.GetTransactions(context.Background(), query.Query{ Limit: 100, }) if !assert.NoError(t, err) { diff --git a/pkg/storage/sqlstorage/sqlite.go b/pkg/storage/sqlstorage/sqlite.go index 9d3f739ddd..4bb27fda27 100644 --- a/pkg/storage/sqlstorage/sqlite.go +++ b/pkg/storage/sqlstorage/sqlite.go @@ -19,6 +19,10 @@ import ( "github.com/numary/ledger/pkg/storage" ) +const ( + SQLCustomFuncMetaCompare = "meta_compare" +) + func init() { errorHandlers[SQLite] = func(err error) error { eerr, ok := err.(sqlite3.Error) @@ -103,7 +107,7 @@ func init() { if err != nil { return err } - err = conn.RegisterFunc("meta_compare", func(metadata string, value string, key ...string) bool { + err = conn.RegisterFunc(SQLCustomFuncMetaCompare, func(metadata string, value string, key ...string) bool { bytes, dataType, _, err := jsonparser.Get([]byte(metadata), key...) if err != nil { return false diff --git a/pkg/storage/sqlstorage/store_bench_test.go b/pkg/storage/sqlstorage/store_bench_test.go index 5dc067785b..c396883bd8 100644 --- a/pkg/storage/sqlstorage/store_bench_test.go +++ b/pkg/storage/sqlstorage/store_bench_test.go @@ -63,8 +63,8 @@ func BenchmarkStore(b *testing.B) { for _, driver := range drivers { for _, tf := range []testingFunction{ { - name: "FindTransactions", - fn: testBenchmarkFindTransactions, + name: "GetTransactions", + fn: testBenchmarkGetTransactions, }, { name: "LastLog", @@ -109,7 +109,7 @@ func BenchmarkStore(b *testing.B) { } } -func testBenchmarkFindTransactions(b *testing.B, store *sqlstorage.Store) { +func testBenchmarkGetTransactions(b *testing.B, store *sqlstorage.Store) { var log *core.Log for i := 0; i < 1000; i++ { tx := core.Transaction{ @@ -138,7 +138,7 @@ func testBenchmarkFindTransactions(b *testing.B, store *sqlstorage.Store) { b.ResetTimer() for n := 0; n < b.N; n++ { - txs, err := store.FindTransactions(context.Background(), query.Query{ + txs, err := store.GetTransactions(context.Background(), query.Query{ Limit: 100, }) assert.NoError(b, err) diff --git a/pkg/storage/sqlstorage/store_test.go b/pkg/storage/sqlstorage/store_test.go index 689d84d9ec..0b3f09527a 100644 --- a/pkg/storage/sqlstorage/store_test.go +++ b/pkg/storage/sqlstorage/store_test.go @@ -58,16 +58,16 @@ func TestStore(t *testing.T) { fn: testAggregateVolumes, }, { - name: "FindAccounts", - fn: testFindAccounts, + name: "GetAccounts", + fn: testGetAccounts, }, { name: "CountTransactions", fn: testCountTransactions, }, { - name: "FindTransactions", - fn: testFindTransactions, + name: "GetTransactions", + fn: testGetTransactions, }, { name: "GetTransaction", @@ -223,7 +223,7 @@ func testAggregateVolumes(t *testing.T, store *sqlstorage.Store) { assert.EqualValues(t, 0, volumes["USD"]["output"]) } -func testFindAccounts(t *testing.T, store *sqlstorage.Store) { +func testGetAccounts(t *testing.T, store *sqlstorage.Store) { account1 := core.NewSetMetadataLog(nil, core.SetMetadata{ TargetType: core.MetaTargetTypeAccount, TargetID: "world", @@ -258,14 +258,14 @@ func testFindAccounts(t *testing.T, store *sqlstorage.Store) { err := store.AppendLog(context.Background(), account1, account2, account3, account4) assert.NoError(t, err) - accounts, err := store.FindAccounts(context.Background(), query.Query{ + accounts, err := store.GetAccounts(context.Background(), query.Query{ Limit: 1, }) assert.NoError(t, err) assert.True(t, accounts.HasMore) assert.Equal(t, 1, accounts.PageSize) - accounts, err = store.FindAccounts(context.Background(), query.Query{ + accounts, err = store.GetAccounts(context.Background(), query.Query{ Limit: 1, After: accounts.Data.([]core.Account)[0].Address, }) @@ -273,7 +273,7 @@ func testFindAccounts(t *testing.T, store *sqlstorage.Store) { assert.True(t, accounts.HasMore) assert.Equal(t, 1, accounts.PageSize) - accounts, err = store.FindAccounts(context.Background(), query.Query{ + accounts, err = store.GetAccounts(context.Background(), query.Query{ Limit: 10, Params: map[string]interface{}{ "address": ".*der.*", @@ -284,7 +284,7 @@ func testFindAccounts(t *testing.T, store *sqlstorage.Store) { assert.Len(t, accounts.Data, 2) assert.Equal(t, 10, accounts.PageSize) - accounts, err = store.FindAccounts(context.Background(), query.Query{ + accounts, err = store.GetAccounts(context.Background(), query.Query{ Limit: 10, Params: map[string]interface{}{ "metadata": map[string]string{ @@ -296,7 +296,7 @@ func testFindAccounts(t *testing.T, store *sqlstorage.Store) { assert.False(t, accounts.HasMore) assert.Len(t, accounts.Data, 1) - accounts, err = store.FindAccounts(context.Background(), query.Query{ + accounts, err = store.GetAccounts(context.Background(), query.Query{ Limit: 10, Params: map[string]interface{}{ "metadata": map[string]string{ @@ -308,7 +308,7 @@ func testFindAccounts(t *testing.T, store *sqlstorage.Store) { assert.False(t, accounts.HasMore) assert.Len(t, accounts.Data, 1) - accounts, err = store.FindAccounts(context.Background(), query.Query{ + accounts, err = store.GetAccounts(context.Background(), query.Query{ Limit: 10, Params: map[string]interface{}{ "metadata": map[string]string{ @@ -320,7 +320,7 @@ func testFindAccounts(t *testing.T, store *sqlstorage.Store) { assert.False(t, accounts.HasMore) assert.Len(t, accounts.Data, 1) - accounts, err = store.FindAccounts(context.Background(), query.Query{ + accounts, err = store.GetAccounts(context.Background(), query.Query{ Limit: 10, Params: map[string]interface{}{ "metadata": map[string]string{ @@ -358,7 +358,7 @@ func testCountTransactions(t *testing.T, store *sqlstorage.Store) { assert.EqualValues(t, 1, countTransactions) } -func testFindTransactions(t *testing.T, store *sqlstorage.Store) { +func testGetTransactions(t *testing.T, store *sqlstorage.Store) { tx1 := core.Transaction{ TransactionData: core.TransactionData{ Postings: []core.Posting{ @@ -409,14 +409,14 @@ func testFindTransactions(t *testing.T, store *sqlstorage.Store) { err := store.AppendLog(context.Background(), log1, log2, log3) assert.NoError(t, err) - cursor, err := store.FindTransactions(context.Background(), query.Query{ + cursor, err := store.GetTransactions(context.Background(), query.Query{ Limit: 1, }) assert.NoError(t, err) assert.Equal(t, 1, cursor.PageSize) assert.True(t, cursor.HasMore) - cursor, err = store.FindTransactions(context.Background(), query.Query{ + cursor, err = store.GetTransactions(context.Background(), query.Query{ After: fmt.Sprint(cursor.Data.([]core.Transaction)[0].ID), Limit: 1, }) @@ -424,7 +424,7 @@ func testFindTransactions(t *testing.T, store *sqlstorage.Store) { assert.Equal(t, 1, cursor.PageSize) assert.True(t, cursor.HasMore) - cursor, err = store.FindTransactions(context.Background(), query.Query{ + cursor, err = store.GetTransactions(context.Background(), query.Query{ Params: map[string]interface{}{ "account": "world", "reference": "tx1", @@ -436,7 +436,7 @@ func testFindTransactions(t *testing.T, store *sqlstorage.Store) { assert.Len(t, cursor.Data, 1) assert.False(t, cursor.HasMore) - cursor, err = store.FindTransactions(context.Background(), query.Query{ + cursor, err = store.GetTransactions(context.Background(), query.Query{ Params: map[string]interface{}{ "source": "central_bank", }, @@ -447,7 +447,7 @@ func testFindTransactions(t *testing.T, store *sqlstorage.Store) { assert.Len(t, cursor.Data, 1) assert.False(t, cursor.HasMore) - cursor, err = store.FindTransactions(context.Background(), query.Query{ + cursor, err = store.GetTransactions(context.Background(), query.Query{ Params: map[string]interface{}{ "destination": "users:1", }, diff --git a/pkg/storage/sqlstorage/transactions.go b/pkg/storage/sqlstorage/transactions.go index 0c8080f83b..a9c6f8349a 100644 --- a/pkg/storage/sqlstorage/transactions.go +++ b/pkg/storage/sqlstorage/transactions.go @@ -34,11 +34,9 @@ func (s *Store) transactionsQuery(p map[string]interface{}) *sqlbuilder.SelectBu return sb } -func (s *Store) findTransactions(ctx context.Context, exec executor, q query.Query) (sharedapi.Cursor, error) { +func (s *Store) getTransactions(ctx context.Context, exec executor, q query.Query) (sharedapi.Cursor, error) { q.Limit = int(math.Max(-1, math.Min(float64(q.Limit), 100))) + 1 - c := sharedapi.Cursor{} - sb := s.transactionsQuery(q.Params) sb.OrderBy("t.id desc") if q.After != "" { @@ -49,7 +47,7 @@ func (s *Store) findTransactions(ctx context.Context, exec executor, q query.Que sqlq, args := sb.BuildWithFlavor(s.schema.Flavor()) rows, err := exec.QueryContext(ctx, sqlq, args...) if err != nil { - return c, s.error(err) + return sharedapi.Cursor{}, s.error(err) } defer func(rows *sql.Rows) { if err := rows.Close(); err != nil { @@ -57,7 +55,7 @@ func (s *Store) findTransactions(ctx context.Context, exec executor, q query.Que } }(rows) - transactions := make([]core.Transaction, 0) + txs := make([]core.Transaction, 0) for rows.Next() { var ( @@ -66,15 +64,14 @@ func (s *Store) findTransactions(ctx context.Context, exec executor, q query.Que ) tx := core.Transaction{} - err := rows.Scan( + if err := rows.Scan( &tx.ID, &ts, &ref, &tx.Metadata, &tx.Postings, - ) - if err != nil { - return c, err + ); err != nil { + return sharedapi.Cursor{}, err } tx.Reference = ref.String if tx.Metadata == nil { @@ -85,80 +82,71 @@ func (s *Store) findTransactions(ctx context.Context, exec executor, q query.Que return sharedapi.Cursor{}, err } tx.Timestamp = timestamp.UTC().Format(time.RFC3339) - transactions = append(transactions, tx) + txs = append(txs, tx) } if rows.Err() != nil { return sharedapi.Cursor{}, s.error(err) } - c.PageSize = q.Limit - 1 - c.HasMore = len(transactions) == q.Limit - if c.HasMore { - transactions = transactions[:len(transactions)-1] + hasMore := false + if len(txs) == q.Limit { + hasMore = true + txs = txs[:len(txs)-1] } - c.Data = transactions - return c, nil + return sharedapi.Cursor{ + PageSize: q.Limit - 1, + HasMore: hasMore, + Data: txs, + }, nil } -func (s *Store) FindTransactions(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { - return s.findTransactions(ctx, s.schema, q) +func (s *Store) GetTransactions(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { + return s.getTransactions(ctx, s.schema, q) } -func (s *Store) getTransaction(ctx context.Context, exec executor, txid uint64) (tx core.Transaction, err error) { +func (s *Store) getTransaction(ctx context.Context, exec executor, txid uint64) (core.Transaction, error) { sb := sqlbuilder.NewSelectBuilder() - sb.Select( - "t.id", - "t.timestamp", - "t.reference", - "t.metadata", - "t.postings", - ) - sb.From(sb.As(s.schema.Table("transactions"), "t")) - sb.Where(sb.Equal("t.id", txid)) - sb.OrderBy("t.id DESC") + sb.Select("id", "timestamp", "reference", "metadata", "postings") + sb.From(s.schema.Table("transactions")) + sb.Where(sb.Equal("id", txid)) + sb.OrderBy("id DESC") sqlq, args := sb.BuildWithFlavor(s.schema.Flavor()) - rows, err := exec.QueryContext(ctx, sqlq, args...) - if err != nil { - return tx, s.error(err) + row := exec.QueryRowContext(ctx, sqlq, args...) + if row.Err() != nil { + return core.Transaction{}, s.error(row.Err()) } - defer func(rows *sql.Rows) { - if err := rows.Close(); err != nil { - panic(err) - } - }(rows) - for rows.Next() { - var ( - ref sql.NullString - ts sql.NullString - ) + var ( + ref sql.NullString + ts sql.NullString + tx core.Transaction + ) - err := rows.Scan( - &tx.ID, - &ts, - &ref, - &tx.Metadata, - &tx.Postings, - ) - if err != nil { - return tx, err + err := row.Scan( + &tx.ID, + &ts, + &ref, + &tx.Metadata, + &tx.Postings, + ) + if err != nil { + if err == sql.ErrNoRows { + return core.Transaction{}, nil } + return core.Transaction{}, err + } - if tx.Metadata == nil { - tx.Metadata = core.Metadata{} - } - t, err := time.Parse(time.RFC3339, ts.String) - if err != nil { - return tx, err - } - tx.Timestamp = t.UTC().Format(time.RFC3339) - tx.Reference = ref.String + if tx.Metadata == nil { + tx.Metadata = core.Metadata{} } - if rows.Err() != nil { - return tx, s.error(rows.Err()) + t, err := time.Parse(time.RFC3339, ts.String) + if err != nil { + return core.Transaction{}, err } + tx.Timestamp = t.UTC().Format(time.RFC3339) + tx.Reference = ref.String return tx, nil } @@ -167,7 +155,7 @@ func (s *Store) GetTransaction(ctx context.Context, txId uint64) (tx core.Transa return s.getTransaction(ctx, s.schema, txId) } -func (s *Store) lastTransaction(ctx context.Context, exec executor) (*core.Transaction, error) { +func (s *Store) getLastTransaction(ctx context.Context, exec executor) (*core.Transaction, error) { sb := sqlbuilder.NewSelectBuilder() sb.Select("id", "timestamp", "reference", "metadata", "postings") sb.From(s.schema.Table("transactions")) @@ -179,12 +167,13 @@ func (s *Store) lastTransaction(ctx context.Context, exec executor) (*core.Trans if row.Err() != nil { return nil, s.error(row.Err()) } + var ( ref sql.NullString ts sql.NullString + tx core.Transaction ) - tx := core.Transaction{} err := row.Scan( &tx.ID, &ts, @@ -199,9 +188,19 @@ func (s *Store) lastTransaction(ctx context.Context, exec executor) (*core.Trans return nil, err } + if tx.Metadata == nil { + tx.Metadata = core.Metadata{} + } + t, err := time.Parse(time.RFC3339, ts.String) + if err != nil { + return nil, err + } + tx.Timestamp = t.UTC().Format(time.RFC3339) + tx.Reference = ref.String + return &tx, nil } -func (s *Store) LastTransaction(ctx context.Context) (*core.Transaction, error) { - return s.lastTransaction(ctx, s.schema) +func (s *Store) GetLastTransaction(ctx context.Context) (*core.Transaction, error) { + return s.getLastTransaction(ctx, s.schema) } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 5aa22470cc..527a4927a2 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -60,14 +60,14 @@ func IsTooManyClientError(err error) bool { } type Store interface { - LastTransaction(ctx context.Context) (*core.Transaction, error) + GetLastTransaction(ctx context.Context) (*core.Transaction, error) CountTransactions(context.Context, query.Query) (uint64, error) - FindTransactions(context.Context, query.Query) (sharedapi.Cursor, error) + GetTransactions(context.Context, query.Query) (sharedapi.Cursor, error) GetTransaction(context.Context, uint64) (core.Transaction, error) GetAccount(context.Context, string) (core.Account, error) AggregateVolumes(context.Context, string) (core.Volumes, error) CountAccounts(context.Context, query.Query) (uint64, error) - FindAccounts(context.Context, query.Query) (sharedapi.Cursor, error) + GetAccounts(context.Context, query.Query) (sharedapi.Cursor, error) AppendLog(ctx context.Context, log ...core.Log) error LastLog(ctx context.Context) (*core.Log, error) @@ -83,7 +83,7 @@ type Store interface { // A no op store. Useful for testing. type noOpStore struct{} -func (n noOpStore) LastTransaction(ctx context.Context) (*core.Transaction, error) { +func (n noOpStore) GetLastTransaction(ctx context.Context) (*core.Transaction, error) { return &core.Transaction{}, nil } @@ -103,7 +103,7 @@ func (n noOpStore) CountTransactions(ctx context.Context, q query.Query) (uint64 return 0, nil } -func (n noOpStore) FindTransactions(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { +func (n noOpStore) GetTransactions(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { return sharedapi.Cursor{}, nil } @@ -127,7 +127,7 @@ func (n noOpStore) CountAccounts(ctx context.Context, q query.Query) (uint64, er return 0, nil } -func (n noOpStore) FindAccounts(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { +func (n noOpStore) GetAccounts(ctx context.Context, q query.Query) (sharedapi.Cursor, error) { return sharedapi.Cursor{}, nil }