From db400d358fe372ffac64ea9a1a528f3a6ab2e130 Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Thu, 5 Feb 2026 12:49:15 +0100 Subject: [PATCH 1/9] query templates --- cmd/serve.go | 4 +- docs/api/README.md | 871 ++++++++++++++++++ docs/events/InsertedSchema.json | 45 + go.mod | 4 +- go.sum | 4 +- internal/README.md | 116 ++- .../bulking/mocks_ledger_controller_test.go | 54 +- internal/api/common/errors.go | 3 +- .../common/mocks_ledger_controller_test.go | 22 +- internal/api/common/pagination.go | 6 - internal/api/module.go | 4 +- internal/api/router.go | 7 +- internal/api/v1/controllers_accounts_count.go | 4 +- .../api/v1/controllers_accounts_count_test.go | 4 +- internal/api/v1/controllers_accounts_list.go | 4 +- .../api/v1/controllers_balances_aggregates.go | 5 +- .../controllers_balances_aggregates_test.go | 11 +- .../api/v1/mocks_ledger_controller_test.go | 54 +- internal/api/v2/common.go | 3 +- internal/api/v2/controllers_accounts_list.go | 3 +- internal/api/v2/controllers_balances.go | 3 +- internal/api/v2/controllers_balances_test.go | 23 +- internal/api/v2/controllers_ledgers_list.go | 2 +- internal/api/v2/controllers_logs_list.go | 3 +- internal/api/v2/controllers_queries_run.go | 78 ++ .../api/v2/controllers_queries_run_test.go | 89 ++ internal/api/v2/controllers_schema_list.go | 3 +- .../api/v2/controllers_transactions_list.go | 3 +- internal/api/v2/controllers_volumes.go | 7 +- internal/api/v2/controllers_volumes_test.go | 29 +- .../api/v2/mocks_ledger_controller_test.go | 54 +- internal/api/v2/routes.go | 9 +- internal/controller/ledger/controller.go | 9 +- .../controller/ledger/controller_default.go | 162 +++- .../ledger/controller_default_test.go | 117 ++- .../ledger/controller_generated_test.go | 54 +- ...ontroller_with_too_many_client_handling.go | 16 + .../ledger/controller_with_traces.go | 35 +- internal/controller/ledger/mocks_test.go | 5 +- internal/controller/ledger/store.go | 4 +- .../controller/ledger/store_generated_test.go | 8 +- internal/controller/system/adapters.go | 2 +- .../common/schema.go => queries/field.go} | 67 +- internal/queries/filter_template.go | 264 ++++++ internal/queries/filter_template_test.go | 333 +++++++ internal/queries/resources.go | 96 ++ internal/queries/schema.go | 63 ++ internal/queries/substitution.go | 219 +++++ internal/queries/substitution_test.go | 116 +++ internal/queries/variables.go | 115 +++ internal/queries/variables_test.go | 106 +++ internal/query_template.go | 149 +++ internal/query_template_test.go | 148 +++ internal/resources.go | 10 + internal/schema.go | 4 + .../48-add-query-templates/notes.yaml | 1 + .../migrations/48-add-query-templates/up.sql | 8 + internal/storage/common/cursor.go | 6 +- internal/storage/common/pagination.go | 30 + internal/storage/common/paginator_column.go | 10 +- internal/storage/common/query.go | 9 + internal/storage/common/resource.go | 43 +- internal/storage/ledger/balances_test.go | 22 +- internal/storage/ledger/moves_test.go | 5 +- internal/storage/ledger/queries.go | 9 - internal/storage/ledger/resource_accounts.go | 16 +- .../ledger/resource_aggregated_balances.go | 23 +- internal/storage/ledger/resource_logs.go | 11 +- internal/storage/ledger/resource_schemas.go | 10 +- .../storage/ledger/resource_transactions.go | 27 +- internal/storage/ledger/resource_volumes.go | 27 +- internal/storage/ledger/store.go | 8 +- internal/storage/ledger/volumes_test.go | 160 ++-- internal/storage/system/resource_ledgers.go | 17 +- openapi.yaml | 203 ++++ openapi/v2.yaml | 203 ++++ pkg/client/.speakeasy/gen.lock | 50 +- pkg/client/.speakeasy/logs/naming.log | 23 +- pkg/client/README.md | 1 + .../components/querytemplateaccountparams.md | 13 + .../components/querytemplatelogparams.md | 13 + .../querytemplatetransactionparams.md | 13 + .../components/querytemplatevolumeparams.md | 15 + .../components/v2accountscursorresponse.md | 1 + .../models/components/v2logscursorresponse.md | 1 + .../docs/models/components/v2queryparams.md | 29 + .../docs/models/components/v2queryresource.md | 11 + .../docs/models/components/v2querytemplate.md | 12 + .../models/components/v2querytemplatevar.md | 9 + pkg/client/docs/models/components/v2schema.md | 3 +- .../docs/models/components/v2schemadata.md | 3 +- .../v2transactionscursorresponse.md | 1 + .../v2volumeswithbalancecursorresponse.md | 1 + .../operations/v2runqueryqueryparamorder.md | 10 + .../models/operations/v2runqueryrequest.md | 18 + .../operations/v2runqueryrequestbody.md | 10 + .../models/operations/v2runqueryresponse.md | 9 + .../operations/v2runqueryresponsebody.md | 31 + pkg/client/docs/sdks/v2/README.md | 76 ++ .../components/v2accountscursorresponse.go | 23 +- .../models/components/v2logscursorresponse.go | 23 +- pkg/client/models/components/v2queryparams.go | 410 +++++++++ .../models/components/v2queryresource.go | 40 + .../models/components/v2querytemplate.go | 46 + .../models/components/v2querytemplatevar.go | 22 + pkg/client/models/components/v2schema.go | 9 + pkg/client/models/components/v2schemadata.go | 9 + .../v2transactionscursorresponse.go | 23 +- .../v2volumeswithbalancecursorresponse.go | 23 +- pkg/client/models/operations/v2runquery.go | 309 +++++++ .../.speakeasy/logs/naming.log | 23 +- pkg/client/v2.go | 220 +++++ test/e2e/api_queries_run_test.go | 185 ++++ tools/generator/go.mod | 5 +- tools/generator/go.sum | 4 +- tools/provisioner/go.mod | 4 +- tools/provisioner/go.sum | 4 +- 117 files changed, 5804 insertions(+), 412 deletions(-) delete mode 100644 internal/api/common/pagination.go create mode 100644 internal/api/v2/controllers_queries_run.go create mode 100644 internal/api/v2/controllers_queries_run_test.go rename internal/{storage/common/schema.go => queries/field.go} (81%) create mode 100644 internal/queries/filter_template.go create mode 100644 internal/queries/filter_template_test.go create mode 100644 internal/queries/resources.go create mode 100644 internal/queries/schema.go create mode 100644 internal/queries/substitution.go create mode 100644 internal/queries/substitution_test.go create mode 100644 internal/queries/variables.go create mode 100644 internal/queries/variables_test.go create mode 100644 internal/query_template.go create mode 100644 internal/query_template_test.go create mode 100644 internal/resources.go create mode 100644 internal/storage/bucket/migrations/48-add-query-templates/notes.yaml create mode 100644 internal/storage/bucket/migrations/48-add-query-templates/up.sql create mode 100644 internal/storage/common/query.go create mode 100644 pkg/client/docs/models/components/querytemplateaccountparams.md create mode 100644 pkg/client/docs/models/components/querytemplatelogparams.md create mode 100644 pkg/client/docs/models/components/querytemplatetransactionparams.md create mode 100644 pkg/client/docs/models/components/querytemplatevolumeparams.md create mode 100644 pkg/client/docs/models/components/v2queryparams.md create mode 100644 pkg/client/docs/models/components/v2queryresource.md create mode 100644 pkg/client/docs/models/components/v2querytemplate.md create mode 100644 pkg/client/docs/models/components/v2querytemplatevar.md create mode 100644 pkg/client/docs/models/operations/v2runqueryqueryparamorder.md create mode 100644 pkg/client/docs/models/operations/v2runqueryrequest.md create mode 100644 pkg/client/docs/models/operations/v2runqueryrequestbody.md create mode 100644 pkg/client/docs/models/operations/v2runqueryresponse.md create mode 100644 pkg/client/docs/models/operations/v2runqueryresponsebody.md create mode 100644 pkg/client/models/components/v2queryparams.go create mode 100644 pkg/client/models/components/v2queryresource.go create mode 100644 pkg/client/models/components/v2querytemplate.go create mode 100644 pkg/client/models/components/v2querytemplatevar.go create mode 100644 pkg/client/models/operations/v2runquery.go create mode 100644 test/e2e/api_queries_run_test.go diff --git a/cmd/serve.go b/cmd/serve.go index d23985b7b..4b6db7a41 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -28,7 +28,6 @@ import ( "github.com/formancehq/go-libs/v3/service" "github.com/formancehq/ledger/internal/api" - "github.com/formancehq/ledger/internal/api/common" "github.com/formancehq/ledger/internal/bus" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" systemcontroller "github.com/formancehq/ledger/internal/controller/system" @@ -36,6 +35,7 @@ import ( "github.com/formancehq/ledger/internal/replication/drivers" "github.com/formancehq/ledger/internal/replication/drivers/alldrivers" "github.com/formancehq/ledger/internal/storage" + storagecommon "github.com/formancehq/ledger/internal/storage/common" systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/formancehq/ledger/internal/tracing" "github.com/formancehq/ledger/internal/worker" @@ -126,7 +126,7 @@ func NewServeCommand() *cobra.Command { MaxSize: cfg.BulkMaxSize, Parallel: cfg.BulkParallel, }, - Pagination: common.PaginationConfig{ + Pagination: storagecommon.PaginationConfig{ MaxPageSize: cfg.MaxPageSize, DefaultPageSize: cfg.DefaultPageSize, }, diff --git a/docs/api/README.md b/docs/api/README.md index 145e582fe..085382451 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -354,6 +354,54 @@ Idempotency-Key: string "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } ``` @@ -446,6 +494,54 @@ Accept: application/json "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } } @@ -525,6 +621,54 @@ Accept: application/json "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } ], @@ -1004,6 +1148,7 @@ Format: `:`, where `` is the field name and `` is ei ```json { + "resource": "accounts", "cursor": { "pageSize": 15, "hasMore": false, @@ -1449,6 +1594,7 @@ Format: `:`, where `` is the field name and `` is ei ```json { + "resource": "transactions", "cursor": { "pageSize": 15, "hasMore": false, @@ -2201,6 +2347,7 @@ Format: `:`, where `` is the field name and `` is ei ```json { + "resource": "volumes", "cursor": { "pageSize": 15, "hasMore": false, @@ -2284,6 +2431,7 @@ Format: `:`, where `` is the field name and `` is ei ```json { + "resource": "logs", "cursor": { "pageSize": 15, "hasMore": false, @@ -2427,6 +2575,212 @@ To perform this operation, you must be authenticated by means of one of the foll Authorization ( Scopes: ledger:write ) +## Run a query template + + + +> Code samples + +```http +POST http://localhost:8080/v2/{ledger}/queries/{id}/run?schemaVersion=v1.0.0 HTTP/1.1 +Host: localhost:8080 +Content-Type: application/json +Accept: application/json + +``` + +`POST /v2/{ledger}/queries/{id}/run` + +Run a query template on a ledger + +> Body parameter + +```json +{ + "cursor": null, + "params": null, + "vars": { + "property1": "string", + "property2": "string" + } +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|ledger|path|string|true|Name of the ledger.| +|schemaVersion|query|string|true|Schema version to use for validation| +|id|path|string|true|Query template ID.| +|pageSize|query|integer(int64)|false|The maximum number of results to return per page.| +|cursor|query|string|false|Parameter used in pagination requests. Maximum page size is set to 15.| +|expand|query|string|false|none| +|pit|query|string(date-time)|false|none| +|order|query|string|false|Deprecated: Use sort param| +|reverse|query|boolean|false|none| +|sort|query|string|false|Sort results using a field name and order (ascending or descending). | +|body|body|object|true|none| +|» cursor|body|any|false|none| +|» params|body|any|false|none| +|» vars|body|object|false|none| +|»» **additionalProperties**|body|string|false|none| + +#### Detailed descriptions + +**pageSize**: The maximum number of results to return per page. + +**cursor**: Parameter used in pagination requests. Maximum page size is set to 15. +Set to the value of next for the next page of results. +Set to the value of previous for the previous page of results. +No other parameters can be set when this parameter is set. + +**sort**: Sort results using a field name and order (ascending or descending). +Format: `:`, where `` is the field name and `` is either `asc` or `desc`. + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|order|effective| + +> Example responses + +> 200 Response + +```json +{ + "resource": "transactions", + "cursor": { + "pageSize": 15, + "hasMore": false, + "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", + "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", + "data": [ + { + "insertedAt": "2019-08-24T14:15:22Z", + "updatedAt": "2019-08-24T14:15:22Z", + "timestamp": "2019-08-24T14:15:22Z", + "postings": [ + { + "amount": 100, + "asset": "COIN", + "destination": "users:002", + "source": "users:001" + } + ], + "reference": "ref:001", + "metadata": { + "admin": "true" + }, + "id": 0, + "reverted": true, + "revertedAt": "2019-08-24T14:15:22Z", + "preCommitVolumes": { + "orders:1": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + }, + "orders:2": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + } + }, + "postCommitVolumes": { + "orders:1": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + }, + "orders:2": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + } + }, + "preCommitEffectiveVolumes": { + "orders:1": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + }, + "orders:2": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + } + }, + "postCommitEffectiveVolumes": { + "orders:1": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + }, + "orders:2": { + "USD": { + "input": 100, + "output": 10, + "balance": 90 + } + } + }, + "template": "string" + } + ] + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)| + +

Response Schema

+ +#### Enumerated Values + +|Property|Value| +|---|---| +|type|NEW_TRANSACTION| +|type|SET_METADATA| +|type|REVERTED_TRANSACTION| +|type|DELETE_METADATA| +|type|INSERTED_SCHEMA| +|targetType|ACCOUNT| +|targetType|TRANSACTION| +|targetType|ACCOUNT| +|targetType|TRANSACTION| +|runtime|experimental-interpreter| +|runtime|machine| +|resource|transactions| +|resource|accounts| +|resource|logs| +|resource|volumes| + + + ## List exporters @@ -3411,6 +3765,7 @@ This operation does not require authentication ```json { + "resource": "accounts", "cursor": { "pageSize": 15, "hasMore": false, @@ -3460,6 +3815,7 @@ This operation does not require authentication |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|resource|any|false|none|none| |cursor|object|true|none|none| |» pageSize|integer(int64)|true|none|none| |» hasMore|boolean|true|none|none| @@ -3476,6 +3832,7 @@ This operation does not require authentication ```json { + "resource": "transactions", "cursor": { "pageSize": 15, "hasMore": false, @@ -3577,6 +3934,7 @@ This operation does not require authentication |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|resource|any|false|none|none| |cursor|object|true|none|none| |» pageSize|integer(int64)|true|none|none| |» hasMore|boolean|true|none|none| @@ -3593,6 +3951,7 @@ This operation does not require authentication ```json { + "resource": "logs", "cursor": { "pageSize": 15, "hasMore": false, @@ -3638,6 +3997,7 @@ This operation does not require authentication |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|resource|any|false|none|none| |cursor|object|true|none|none| |» pageSize|integer(int64)|true|none|none| |» hasMore|boolean|true|none|none| @@ -3729,6 +4089,7 @@ This operation does not require authentication ```json { + "resource": "volumes", "cursor": { "pageSize": 15, "hasMore": false, @@ -3752,6 +4113,7 @@ This operation does not require authentication |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|resource|any|false|none|none| |cursor|object|true|none|none| |» pageSize|integer(int64)|true|none|none| |» hasMore|boolean|true|none|none| @@ -4576,6 +4938,54 @@ continued "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } } @@ -6324,6 +6734,226 @@ Transaction templates |---|---|---|---|---| |**additionalProperties**|[V2TransactionTemplate](#schemav2transactiontemplate)|false|none|none| +

V2QueryTemplateVar

+ + + + + + +```json +{ + "type": null, + "default": null +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|type|any|false|none|none| +|default|any|false|none|none| + +

V2QueryResource

+ + + + + + +```json +"transactions" + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|string|false|none|none| + +#### Enumerated Values + +|Property|Value| +|---|---| +|*anonymous*|transactions| +|*anonymous*|accounts| +|*anonymous*|logs| +|*anonymous*|volumes| + +

V2QueryParams

+ + + + + + +```json +{ + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|pageSize|integer(int64)|false|none|The maximum number of results to return per page.| +|cursor|string|false|none|Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.| +|expand|string|false|none|none| +|pit|string(date-time)|false|none|none| +|sort|[#/components/parameters/sort](#schema#/components/parameters/sort)|false|none|Sort results using a field name and order (ascending or descending).
Format: `:`, where `` is the field name and `` is either `asc` or `desc`.| + +oneOf + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» resource|any|false|none|none| + +xor + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» resource|any|false|none|none| + +xor + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» resource|any|false|none|none| + +xor + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» resource|any|false|none|none| +|» useInsertionDate|any|false|none|none| +|» groupLvl|any|false|none|none| + +

V2QueryTemplate

+ + + + + + +```json +{ + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|any|false|none|none| +|resource|[V2QueryResource](#schemav2queryresource)|false|none|none| +|params|[V2QueryParams](#schemav2queryparams)|false|none|none| +|vars|object|false|none|none| +|» **additionalProperties**|[V2QueryTemplateVar](#schemav2querytemplatevar)|false|none|none| +|body|object|false|none|none| + +

V2QueryTemplates

+ + + + + + +```json +{ + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } +} + +``` + +Query templates + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|**additionalProperties**|[V2QueryTemplate](#schemav2querytemplate)|false|none|none| +

V2SchemaData

@@ -6351,6 +6981,54 @@ Transaction templates "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } @@ -6364,6 +7042,7 @@ Schema data structure for ledger schemas |---|---|---|---|---| |chart|[V2ChartOfAccounts](#schemav2chartofaccounts)|true|none|Chart of account| |transactions|[V2TransactionTemplates](#schemav2transactiontemplates)|true|none|Transaction templates| +|queries|[V2QueryTemplates](#schemav2querytemplates)|false|none|Query templates|

V2Schema

@@ -6394,6 +7073,54 @@ Schema data structure for ledger schemas "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } @@ -6447,6 +7174,54 @@ and "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } } @@ -6491,6 +7266,54 @@ and "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } ], @@ -6540,6 +7363,54 @@ and "script": "string", "runtime": "experimental-interpreter" } + }, + "queries": { + "property1": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + }, + "property2": { + "name": null, + "resource": "transactions", + "params": { + "pageSize": 100, + "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==", + "expand": "string", + "pit": "2019-08-24T14:15:22Z", + "sort": {}, + "resource": "accounts" + }, + "vars": { + "property1": { + "type": null, + "default": null + }, + "property2": { + "type": null, + "default": null + } + }, + "body": {} + } } } ], diff --git a/docs/events/InsertedSchema.json b/docs/events/InsertedSchema.json index be583229c..ccd14adef 100644 --- a/docs/events/InsertedSchema.json +++ b/docs/events/InsertedSchema.json @@ -112,6 +112,35 @@ "schema" ] }, + "QueryTemplate": { + "properties": { + "description": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "params": true, + "vars": { + "additionalProperties": { + "$ref": "#/$defs/VarDecl" + }, + "type": "object" + }, + "body": true + }, + "additionalProperties": false, + "type": "object", + "required": [ + "resource" + ] + }, + "QueryTemplates": { + "additionalProperties": { + "$ref": "#/$defs/QueryTemplate" + }, + "type": "object" + }, "Schema": { "properties": { "chart": { @@ -120,6 +149,9 @@ "transactions": { "$ref": "#/$defs/TransactionTemplates" }, + "queries": { + "$ref": "#/$defs/QueryTemplates" + }, "version": { "type": "string" }, @@ -132,6 +164,7 @@ "required": [ "chart", "transactions", + "queries", "version", "createdAt" ] @@ -165,6 +198,18 @@ "$ref": "#/$defs/TransactionTemplate" }, "type": "object" + }, + "VarDecl": { + "properties": { + "Type": true, + "Default": true + }, + "additionalProperties": false, + "type": "object", + "required": [ + "Type", + "Default" + ] } } } \ No newline at end of file diff --git a/go.mod b/go.mod index 2f6a6c0c0..783f0d02e 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( require ( github.com/ClickHouse/clickhouse-go/v2 v2.37.2 - github.com/formancehq/go-libs/v3 v3.5.0 + github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07 github.com/go-jose/go-jose/v4 v4.0.5 github.com/iancoleman/strcase v0.3.0 github.com/mitchellh/mapstructure v1.5.0 @@ -125,7 +125,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/containerd/continuity v0.4.5 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect github.com/docker/cli v27.4.1+incompatible // indirect diff --git a/go.sum b/go.sum index 9e9781bd4..3233c273f 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v3 v3.5.0 h1:8RLGsQ2EuAMSL89rxbjcIyvEM9tavJIlomPcvIqRNzA= -github.com/formancehq/go-libs/v3 v3.5.0/go.mod h1:Lr0qE3ioCTFlm+BuXSwB7qpGF12/IfKYOpFvszTFRJk= +github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07 h1:xATZNNolHaP0JRjZGxgNsGKdKLV5yUU9r80bKm9HQN4= +github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07/go.mod h1:3kzr8nMPSbUEaQzaSwesUgn8QFwsbx2NHQC0P1vXat4= github.com/formancehq/numscript v0.0.21 h1:aeudLAKGL8u4TYBiJkZFKw0ElC4V44iJCf/xOGMJKIA= github.com/formancehq/numscript v0.0.21/go.mod h1:hC/VY5Vg04F5QkgdPPc6z/YsS/vh8V1qVJVa1VWnYMA= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= diff --git a/internal/README.md b/internal/README.md index cd10496ac..e858b1ce1 100644 --- a/internal/README.md +++ b/internal/README.md @@ -83,6 +83,8 @@ import "github.com/formancehq/ledger/internal" - [func NewExporter\(configuration ExporterConfiguration\) Exporter](<#NewExporter>) - [type ExporterConfiguration](<#ExporterConfiguration>) - [func NewExporterConfiguration\(driver string, config json.RawMessage\) ExporterConfiguration](<#NewExporterConfiguration>) +- [type GetAggregatedVolumesOptions](<#GetAggregatedVolumesOptions>) +- [type GetVolumesOptions](<#GetVolumesOptions>) - [type InsertedSchema](<#InsertedSchema>) - [func \(p InsertedSchema\) NeedsSchema\(\) bool](<#InsertedSchema.NeedsSchema>) - [func \(u InsertedSchema\) Type\(\) LogType](<#InsertedSchema.Type>) @@ -133,6 +135,13 @@ import "github.com/formancehq/ledger/internal" - [type Postings](<#Postings>) - [func \(p Postings\) Reverse\(\) Postings](<#Postings.Reverse>) - [func \(p Postings\) Validate\(\) \(int, error\)](<#Postings.Validate>) +- [type QueryTemplate](<#QueryTemplate>) + - [func \(q QueryTemplate\) Validate\(\) error](<#QueryTemplate.Validate>) +- [type QueryTemplateParams](<#QueryTemplateParams>) + - [func \(q QueryTemplateParams\[Opts\]\) Overwrite\(others ...json.RawMessage\) \(\*QueryTemplateParams\[Opts\], error\)](<#QueryTemplateParams[Opts].Overwrite>) + - [func \(p \*QueryTemplateParams\[Opts\]\) UnmarshalJSON\(b \[\]byte\) error](<#QueryTemplateParams[Opts].UnmarshalJSON>) +- [type QueryTemplates](<#QueryTemplates>) + - [func \(t QueryTemplates\) Validate\(\) error](<#QueryTemplates.Validate>) - [type RevertedTransaction](<#RevertedTransaction>) - [func \(r RevertedTransaction\) GetMemento\(\) any](<#RevertedTransaction.GetMemento>) - [func \(p RevertedTransaction\) NeedsSchema\(\) bool](<#RevertedTransaction.NeedsSchema>) @@ -992,6 +1001,29 @@ func NewExporterConfiguration(driver string, config json.RawMessage) ExporterCon + +## type [GetAggregatedVolumesOptions]() + + + +```go +type GetAggregatedVolumesOptions struct { + UseInsertionDate bool `json:"useInsertionDate"` +} +``` + + +## type [GetVolumesOptions]() + + + +```go +type GetVolumesOptions struct { + UseInsertionDate bool `json:"useInsertionDate"` + GroupLvl int `json:"groupLvl"` +} +``` + ## type [InsertedSchema]() @@ -1513,6 +1545,83 @@ func (p Postings) Validate() (int, error) + +## type [QueryTemplate]() + + + +```go +type QueryTemplate struct { + Description string `json:"description,omitempty"` + Resource queries.ResourceKind `json:"resource"` + Params json.RawMessage `json:"params,omitempty"` + Vars map[string]queries.VarDecl `json:"vars,omitempty"` + Body json.RawMessage `json:"body,omitempty"` +} +``` + + +### func \(QueryTemplate\) [Validate]() + +```go +func (q QueryTemplate) Validate() error +``` + +Validate a query template + + +## type [QueryTemplateParams]() + + + +```go +type QueryTemplateParams[Opts any] struct { + PIT *time.Time + OOT *time.Time + Expand []string + Opts Opts + SortColumn string + SortOrder *bunpaginate.Order + PageSize uint +} +``` + + +### func \(QueryTemplateParams\[Opts\]\) [Overwrite]() + +```go +func (q QueryTemplateParams[Opts]) Overwrite(others ...json.RawMessage) (*QueryTemplateParams[Opts], error) +``` + + + + +### func \(\*QueryTemplateParams\[Opts\]\) [UnmarshalJSON]() + +```go +func (p *QueryTemplateParams[Opts]) UnmarshalJSON(b []byte) error +``` + + + + +## type [QueryTemplates]() + + + +```go +type QueryTemplates map[string]QueryTemplate +``` + + +### func \(QueryTemplates\) [Validate]() + +```go +func (t QueryTemplates) Validate() error +``` + + + ## type [RevertedTransaction]() @@ -1629,7 +1738,7 @@ func (s SavedMetadata) ValidateWithSchema(schema Schema) error -## type [Schema]() +## type [Schema]() @@ -1644,7 +1753,7 @@ type Schema struct { ``` -### func [NewSchema]() +### func [NewSchema]() ```go func NewSchema(version string, data SchemaData) (Schema, error) @@ -1653,7 +1762,7 @@ func NewSchema(version string, data SchemaData) (Schema, error) -## type [SchemaData]() +## type [SchemaData]() @@ -1661,6 +1770,7 @@ func NewSchema(version string, data SchemaData) (Schema, error) type SchemaData struct { Chart ChartOfAccounts `json:"chart" bun:"chart"` Transactions TransactionTemplates `json:"transactions" bun:"transactions"` + Queries QueryTemplates `json:"queries" bun:"queries"` } ``` diff --git a/internal/api/bulking/mocks_ledger_controller_test.go b/internal/api/bulking/mocks_ledger_controller_test.go index f0c8d9416..79df1749d 100644 --- a/internal/api/bulking/mocks_ledger_controller_test.go +++ b/internal/api/bulking/mocks_ledger_controller_test.go @@ -16,8 +16,8 @@ import ( migrations "github.com/formancehq/go-libs/v3/migrations" ledger "github.com/formancehq/ledger/internal" ledger0 "github.com/formancehq/ledger/internal/controller/ledger" + queries "github.com/formancehq/ledger/internal/queries" common "github.com/formancehq/ledger/internal/storage/common" - ledger1 "github.com/formancehq/ledger/internal/storage/ledger" bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" ) @@ -401,7 +401,7 @@ func (c *LedgerControllerGetAccountCall) DoAndReturn(f func(context.Context, com } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -428,13 +428,13 @@ func (c *LedgerControllerGetAggregatedBalancesCall) Return(arg0 ledger.BalancesB } // Do rewrite *gomock.Call.Do -func (c *LedgerControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { +func (c *LedgerControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *LedgerControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { +func (c *LedgerControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -596,7 +596,7 @@ func (c *LedgerControllerGetTransactionCall) DoAndReturn(f func(context.Context, } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -623,13 +623,13 @@ func (c *LedgerControllerGetVolumesWithBalancesCall) Return(arg0 *bunpaginate.Cu } // Do rewrite *gomock.Call.Do -func (c *LedgerControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { +func (c *LedgerControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *LedgerControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { +func (c *LedgerControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -1066,6 +1066,46 @@ func (c *LedgerControllerRollbackCall) DoAndReturn(f func(context.Context) error return c } +// RunQuery mocks base method. +func (m *LedgerController) RunQuery(ctx context.Context, schemaVersion, queryId string, runQuery common.RunQuery, defaultPageSize common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunQuery", ctx, schemaVersion, queryId, runQuery, defaultPageSize) + ret0, _ := ret[0].(*queries.ResourceKind) + ret1, _ := ret[1].(*bunpaginate.Cursor[any]) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RunQuery indicates an expected call of RunQuery. +func (mr *LedgerControllerMockRecorder) RunQuery(ctx, schemaVersion, queryId, runQuery, defaultPageSize any) *LedgerControllerRunQueryCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunQuery", reflect.TypeOf((*LedgerController)(nil).RunQuery), ctx, schemaVersion, queryId, runQuery, defaultPageSize) + return &LedgerControllerRunQueryCall{Call: call} +} + +// LedgerControllerRunQueryCall wrap *gomock.Call +type LedgerControllerRunQueryCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *LedgerControllerRunQueryCall) Return(arg0 *queries.ResourceKind, arg1 *bunpaginate.Cursor[any], arg2 error) *LedgerControllerRunQueryCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *LedgerControllerRunQueryCall) Do(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *LedgerControllerRunQueryCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *LedgerControllerRunQueryCall) DoAndReturn(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *LedgerControllerRunQueryCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SaveAccountMetadata mocks base method. func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, bool, error) { m.ctrl.T.Helper() diff --git a/internal/api/common/errors.go b/internal/api/common/errors.go index 0b6a73898..b6af0e730 100644 --- a/internal/api/common/errors.go +++ b/internal/api/common/errors.go @@ -71,7 +71,8 @@ func HandleCommonPaginationErrors(w http.ResponseWriter, r *http.Request, err er switch { case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledger.ErrMissingFeature{}) || - errors.Is(err, storagecommon.ErrNotPaginatedField{}): + errors.Is(err, storagecommon.ErrNotPaginatedField{}) || + errors.Is(err, ledgercontroller.ErrSchemaValidationError{}): api.BadRequest(w, ErrValidation, err) default: HandleCommonErrors(w, r, err) diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go index 65cf93e22..829481b76 100644 --- a/internal/api/common/mocks_ledger_controller_test.go +++ b/internal/api/common/mocks_ledger_controller_test.go @@ -16,8 +16,8 @@ import ( migrations "github.com/formancehq/go-libs/v3/migrations" ledger "github.com/formancehq/ledger/internal" ledger0 "github.com/formancehq/ledger/internal/controller/ledger" + queries "github.com/formancehq/ledger/internal/queries" common "github.com/formancehq/ledger/internal/storage/common" - ledger1 "github.com/formancehq/ledger/internal/storage/ledger" bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" ) @@ -185,7 +185,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -260,7 +260,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -442,6 +442,22 @@ func (mr *LedgerControllerMockRecorder) Rollback(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*LedgerController)(nil).Rollback), ctx) } +// RunQuery mocks base method. +func (m *LedgerController) RunQuery(ctx context.Context, schemaVersion, queryId string, runQuery common.RunQuery, defaultPageSize common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunQuery", ctx, schemaVersion, queryId, runQuery, defaultPageSize) + ret0, _ := ret[0].(*queries.ResourceKind) + ret1, _ := ret[1].(*bunpaginate.Cursor[any]) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RunQuery indicates an expected call of RunQuery. +func (mr *LedgerControllerMockRecorder) RunQuery(ctx, schemaVersion, queryId, runQuery, defaultPageSize any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunQuery", reflect.TypeOf((*LedgerController)(nil).RunQuery), ctx, schemaVersion, queryId, runQuery, defaultPageSize) +} + // SaveAccountMetadata mocks base method. func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, bool, error) { m.ctrl.T.Helper() diff --git a/internal/api/common/pagination.go b/internal/api/common/pagination.go deleted file mode 100644 index 163a29616..000000000 --- a/internal/api/common/pagination.go +++ /dev/null @@ -1,6 +0,0 @@ -package common - -type PaginationConfig struct { - MaxPageSize uint64 - DefaultPageSize uint64 -} diff --git a/internal/api/module.go b/internal/api/module.go index ad93134cd..47a785e8c 100644 --- a/internal/api/module.go +++ b/internal/api/module.go @@ -11,8 +11,8 @@ import ( "github.com/formancehq/go-libs/v3/health" "github.com/formancehq/ledger/internal/api/bulking" - "github.com/formancehq/ledger/internal/api/common" "github.com/formancehq/ledger/internal/controller/system" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) type BulkConfig struct { @@ -24,7 +24,7 @@ type Config struct { Version string Debug bool Bulk BulkConfig - Pagination common.PaginationConfig + Pagination storagecommon.PaginationConfig Exporters bool } diff --git a/internal/api/router.go b/internal/api/router.go index ad89ef3fa..59f053e06 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -24,6 +24,7 @@ import ( v1 "github.com/formancehq/ledger/internal/api/v1" v2 "github.com/formancehq/ledger/internal/api/v2" "github.com/formancehq/ledger/internal/controller/system" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) // todo: refine textual errors @@ -118,7 +119,7 @@ type routerOptions struct { meterProvider metric.MeterProvider bulkMaxSize int bulkerFactory bulking.BulkerFactory - paginationConfig common.PaginationConfig + paginationConfig storagecommon.PaginationConfig exporters bool } @@ -142,7 +143,7 @@ func WithBulkerFactory(bf bulking.BulkerFactory) RouterOption { } } -func WithPaginationConfiguration(paginationConfig common.PaginationConfig) RouterOption { +func WithPaginationConfiguration(paginationConfig storagecommon.PaginationConfig) RouterOption { return func(ro *routerOptions) { ro.paginationConfig = paginationConfig } @@ -164,7 +165,7 @@ var defaultRouterOptions = []RouterOption{ WithTracer(nooptracer.Tracer{}), WithMeterProvider(noopmetrics.MeterProvider{}), WithBulkMaxSize(DefaultBulkMaxSize), - WithPaginationConfiguration(common.PaginationConfig{ + WithPaginationConfiguration(storagecommon.PaginationConfig{ MaxPageSize: bunpaginate.MaxPageSize, DefaultPageSize: bunpaginate.QueryDefaultPageSize, }), diff --git a/internal/api/v1/controllers_accounts_count.go b/internal/api/v1/controllers_accounts_count.go index d0e5d948b..be1035d15 100644 --- a/internal/api/v1/controllers_accounts_count.go +++ b/internal/api/v1/controllers_accounts_count.go @@ -9,7 +9,7 @@ import ( "github.com/formancehq/ledger/internal/api/common" storagecommon "github.com/formancehq/ledger/internal/storage/common" - "github.com/formancehq/ledger/internal/storage/ledger" + ledgerstorage "github.com/formancehq/ledger/internal/storage/ledger" ) func countAccounts(w http.ResponseWriter, r *http.Request) { @@ -30,7 +30,7 @@ func countAccounts(w http.ResponseWriter, r *http.Request) { count, err := l.CountAccounts(r.Context(), *rq) if err != nil { switch { - case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledger.ErrMissingFeature{}): + case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstorage.ErrMissingFeature{}): api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) diff --git a/internal/api/v1/controllers_accounts_count_test.go b/internal/api/v1/controllers_accounts_count_test.go index e0c40b6f3..fd89c9959 100644 --- a/internal/api/v1/controllers_accounts_count_test.go +++ b/internal/api/v1/controllers_accounts_count_test.go @@ -17,7 +17,7 @@ import ( "github.com/formancehq/ledger/internal/api/common" storagecommon "github.com/formancehq/ledger/internal/storage/common" - "github.com/formancehq/ledger/internal/storage/ledger" + ledgerstorage "github.com/formancehq/ledger/internal/storage/ledger" ) func TestAccountsCount(t *testing.T) { @@ -89,7 +89,7 @@ func TestAccountsCount(t *testing.T) { expectStatusCode: http.StatusBadRequest, expectedErrorCode: common.ErrValidation, expectBackendCall: true, - returnErr: ledger.ErrMissingFeature{}, + returnErr: ledgerstorage.ErrMissingFeature{}, expectQuery: storagecommon.ResourceQuery[any]{}, }, { diff --git a/internal/api/v1/controllers_accounts_list.go b/internal/api/v1/controllers_accounts_list.go index 4b3072c45..d097233ba 100644 --- a/internal/api/v1/controllers_accounts_list.go +++ b/internal/api/v1/controllers_accounts_list.go @@ -9,7 +9,7 @@ import ( "github.com/formancehq/ledger/internal/api/common" storagecommon "github.com/formancehq/ledger/internal/storage/common" - "github.com/formancehq/ledger/internal/storage/ledger" + ledgerstorage "github.com/formancehq/ledger/internal/storage/ledger" ) func listAccounts(w http.ResponseWriter, r *http.Request) { @@ -33,7 +33,7 @@ func listAccounts(w http.ResponseWriter, r *http.Request) { cursor, err := l.ListAccounts(r.Context(), rq) if err != nil { switch { - case errors.Is(err, ledger.ErrMissingFeature{}): + case errors.Is(err, ledgerstorage.ErrMissingFeature{}): api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) diff --git a/internal/api/v1/controllers_balances_aggregates.go b/internal/api/v1/controllers_balances_aggregates.go index e7aa0bb32..9885cdbbf 100644 --- a/internal/api/v1/controllers_balances_aggregates.go +++ b/internal/api/v1/controllers_balances_aggregates.go @@ -6,8 +6,8 @@ import ( "github.com/formancehq/go-libs/v3/api" "github.com/formancehq/go-libs/v3/query" + ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ) func buildAggregatedBalancesQuery(r *http.Request) query.Builder { @@ -19,8 +19,7 @@ func buildAggregatedBalancesQuery(r *http.Request) query.Builder { } func getBalancesAggregated(w http.ResponseWriter, r *http.Request) { - - rq, err := getResourceQuery[ledgerstore.GetAggregatedVolumesOptions](r, func(q *ledgerstore.GetAggregatedVolumesOptions) error { + rq, err := getResourceQuery(r, func(q *ledger.GetAggregatedVolumesOptions) error { q.UseInsertionDate = true return nil diff --git a/internal/api/v1/controllers_balances_aggregates_test.go b/internal/api/v1/controllers_balances_aggregates_test.go index 1e6870a6d..6cb7cd7fc 100644 --- a/internal/api/v1/controllers_balances_aggregates_test.go +++ b/internal/api/v1/controllers_balances_aggregates_test.go @@ -17,7 +17,6 @@ import ( ledger "github.com/formancehq/ledger/internal" storagecommon "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ) func TestBalancesAggregates(t *testing.T) { @@ -26,14 +25,14 @@ func TestBalancesAggregates(t *testing.T) { type testCase struct { name string queryParams url.Values - expectQuery storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions] + expectQuery storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions] } testCases := []testCase{ { name: "nominal", - expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{ + expectQuery: storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{ UseInsertionDate: true, }, }, @@ -43,8 +42,8 @@ func TestBalancesAggregates(t *testing.T) { queryParams: url.Values{ "address": []string{"foo"}, }, - expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{ + expectQuery: storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{ UseInsertionDate: true, }, Builder: query.Match("address", "foo"), diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go index 69ffd7077..116f3b6e4 100644 --- a/internal/api/v1/mocks_ledger_controller_test.go +++ b/internal/api/v1/mocks_ledger_controller_test.go @@ -16,8 +16,8 @@ import ( migrations "github.com/formancehq/go-libs/v3/migrations" ledger "github.com/formancehq/ledger/internal" ledger0 "github.com/formancehq/ledger/internal/controller/ledger" + queries "github.com/formancehq/ledger/internal/queries" common "github.com/formancehq/ledger/internal/storage/common" - ledger1 "github.com/formancehq/ledger/internal/storage/ledger" bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" ) @@ -401,7 +401,7 @@ func (c *LedgerControllerGetAccountCall) DoAndReturn(f func(context.Context, com } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -428,13 +428,13 @@ func (c *LedgerControllerGetAggregatedBalancesCall) Return(arg0 ledger.BalancesB } // Do rewrite *gomock.Call.Do -func (c *LedgerControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { +func (c *LedgerControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *LedgerControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { +func (c *LedgerControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -596,7 +596,7 @@ func (c *LedgerControllerGetTransactionCall) DoAndReturn(f func(context.Context, } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -623,13 +623,13 @@ func (c *LedgerControllerGetVolumesWithBalancesCall) Return(arg0 *bunpaginate.Cu } // Do rewrite *gomock.Call.Do -func (c *LedgerControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { +func (c *LedgerControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *LedgerControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { +func (c *LedgerControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -1066,6 +1066,46 @@ func (c *LedgerControllerRollbackCall) DoAndReturn(f func(context.Context) error return c } +// RunQuery mocks base method. +func (m *LedgerController) RunQuery(ctx context.Context, schemaVersion, queryId string, runQuery common.RunQuery, defaultPageSize common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunQuery", ctx, schemaVersion, queryId, runQuery, defaultPageSize) + ret0, _ := ret[0].(*queries.ResourceKind) + ret1, _ := ret[1].(*bunpaginate.Cursor[any]) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RunQuery indicates an expected call of RunQuery. +func (mr *LedgerControllerMockRecorder) RunQuery(ctx, schemaVersion, queryId, runQuery, defaultPageSize any) *LedgerControllerRunQueryCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunQuery", reflect.TypeOf((*LedgerController)(nil).RunQuery), ctx, schemaVersion, queryId, runQuery, defaultPageSize) + return &LedgerControllerRunQueryCall{Call: call} +} + +// LedgerControllerRunQueryCall wrap *gomock.Call +type LedgerControllerRunQueryCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *LedgerControllerRunQueryCall) Return(arg0 *queries.ResourceKind, arg1 *bunpaginate.Cursor[any], arg2 error) *LedgerControllerRunQueryCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *LedgerControllerRunQueryCall) Do(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *LedgerControllerRunQueryCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *LedgerControllerRunQueryCall) DoAndReturn(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *LedgerControllerRunQueryCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SaveAccountMetadata mocks base method. func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, bool, error) { m.ctrl.T.Helper() diff --git a/internal/api/v2/common.go b/internal/api/v2/common.go index 64442f6fc..a33491bf8 100644 --- a/internal/api/v2/common.go +++ b/internal/api/v2/common.go @@ -14,7 +14,6 @@ import ( "github.com/formancehq/go-libs/v3/query" "github.com/formancehq/go-libs/v3/time" - "github.com/formancehq/ledger/internal/api/common" storagecommon "github.com/formancehq/ledger/internal/storage/common" ) @@ -75,7 +74,7 @@ func getExpand(r *http.Request) []string { func getPaginatedQuery[Options any]( r *http.Request, - paginationConfig common.PaginationConfig, + paginationConfig storagecommon.PaginationConfig, column string, order bunpaginate.Order, modifiers ...func(resourceQuery *storagecommon.ResourceQuery[Options]), diff --git a/internal/api/v2/controllers_accounts_list.go b/internal/api/v2/controllers_accounts_list.go index adc201844..5085442ef 100644 --- a/internal/api/v2/controllers_accounts_list.go +++ b/internal/api/v2/controllers_accounts_list.go @@ -8,9 +8,10 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) -func listAccounts(paginationConfig common.PaginationConfig) http.HandlerFunc { +func listAccounts(paginationConfig storagecommon.PaginationConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) diff --git a/internal/api/v2/controllers_balances.go b/internal/api/v2/controllers_balances.go index 2f2777ee5..59f45d6b7 100644 --- a/internal/api/v2/controllers_balances.go +++ b/internal/api/v2/controllers_balances.go @@ -6,6 +6,7 @@ import ( "github.com/formancehq/go-libs/v3/api" + ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" storagecommon "github.com/formancehq/ledger/internal/storage/common" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" @@ -13,7 +14,7 @@ import ( func readBalancesAggregated(w http.ResponseWriter, r *http.Request) { - rq, err := getResourceQuery[ledgerstore.GetAggregatedVolumesOptions](r, func(options *ledgerstore.GetAggregatedVolumesOptions) error { + rq, err := getResourceQuery[ledger.GetAggregatedVolumesOptions](r, func(options *ledger.GetAggregatedVolumesOptions) error { options.UseInsertionDate = api.QueryParamBool(r, "use_insertion_date") || api.QueryParamBool(r, "useInsertionDate") return nil diff --git a/internal/api/v2/controllers_balances_test.go b/internal/api/v2/controllers_balances_test.go index 561bcd1ce..5f23e9553 100644 --- a/internal/api/v2/controllers_balances_test.go +++ b/internal/api/v2/controllers_balances_test.go @@ -18,7 +18,6 @@ import ( ledger "github.com/formancehq/ledger/internal" storagecommon "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ) func TestBalancesAggregates(t *testing.T) { @@ -28,7 +27,7 @@ func TestBalancesAggregates(t *testing.T) { name string queryParams url.Values body string - expectQuery storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions] + expectQuery storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions] } now := time.Now() @@ -36,8 +35,8 @@ func TestBalancesAggregates(t *testing.T) { testCases := []testCase{ { name: "nominal", - expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{}, + expectQuery: storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{}, PIT: &now, Expand: make([]string, 0), }, @@ -45,8 +44,8 @@ func TestBalancesAggregates(t *testing.T) { { name: "using address", body: `{"$match": {"address": "foo"}}`, - expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{}, + expectQuery: storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{}, PIT: &now, Builder: query.Match("address", "foo"), Expand: make([]string, 0), @@ -55,8 +54,8 @@ func TestBalancesAggregates(t *testing.T) { { name: "using exists metadata filter", body: `{"$exists": {"metadata": "foo"}}`, - expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{}, + expectQuery: storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{}, PIT: &now, Builder: query.Exists("metadata", "foo"), Expand: make([]string, 0), @@ -67,8 +66,8 @@ func TestBalancesAggregates(t *testing.T) { queryParams: url.Values{ "pit": []string{now.Format(time.RFC3339Nano)}, }, - expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{}, + expectQuery: storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{}, PIT: &now, Expand: make([]string, 0), }, @@ -79,8 +78,8 @@ func TestBalancesAggregates(t *testing.T) { "pit": []string{now.Format(time.RFC3339Nano)}, "useInsertionDate": []string{"true"}, }, - expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{ + expectQuery: storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{ UseInsertionDate: true, }, PIT: &now, diff --git a/internal/api/v2/controllers_ledgers_list.go b/internal/api/v2/controllers_ledgers_list.go index a2ecbd02f..ef3a66f0f 100644 --- a/internal/api/v2/controllers_ledgers_list.go +++ b/internal/api/v2/controllers_ledgers_list.go @@ -16,7 +16,7 @@ import ( // The handler applies the provided pagination configuration (sorted by "id" ascending), // reads the "includeDeleted" query parameter to include deleted ledgers when set, // invokes the controller's ListLedgers, and renders the resulting paginated cursor. -func listLedgers(b system.Controller, paginationConfig common.PaginationConfig) http.HandlerFunc { +func listLedgers(b system.Controller, paginationConfig storagecommon.PaginationConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rq, err := getPaginatedQuery[systemstore.ListLedgersQueryPayload]( diff --git a/internal/api/v2/controllers_logs_list.go b/internal/api/v2/controllers_logs_list.go index 3346d7862..875853e2d 100644 --- a/internal/api/v2/controllers_logs_list.go +++ b/internal/api/v2/controllers_logs_list.go @@ -8,9 +8,10 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) -func listLogs(paginationConfig common.PaginationConfig) http.HandlerFunc { +func listLogs(paginationConfig storagecommon.PaginationConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) diff --git a/internal/api/v2/controllers_queries_run.go b/internal/api/v2/controllers_queries_run.go new file mode 100644 index 000000000..4c42b20cc --- /dev/null +++ b/internal/api/v2/controllers_queries_run.go @@ -0,0 +1,78 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi/v5" + _ "github.com/pkg/errors" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" + "github.com/formancehq/ledger/internal/queries" + storage "github.com/formancehq/ledger/internal/storage/common" +) + +func runQuery(paginationConfig storage.PaginationConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + common.WithBody(w, r, func(payload storage.RunQuery) { + l := common.LedgerFromContext(r.Context()) + + schemaVersion := r.URL.Query().Get("schemaVersion") + queryId := chi.URLParam(r, "id") + + resource, cursor, err := l.RunQuery(r.Context(), schemaVersion, queryId, payload, paginationConfig) + if err != nil { + common.HandleCommonPaginationErrors(w, r, err) + return + } + + err = getJsonResponse(r, w, *resource, *cursor) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + }) + } +} + +func getJsonResponse(r *http.Request, w http.ResponseWriter, resource queries.ResourceKind, cursor bunpaginate.Cursor[any]) error { + renderedCursor := *bunpaginate.MapCursor(&cursor, func(item any) any { + switch v := item.(type) { + case ledger.Transaction: + return renderTransaction(r, v) + case ledger.Account: + return renderAccount(r, v) + case ledger.VolumesWithBalanceByAssetByAccount: + return renderVolumesWithBalances(r, v) + case ledger.Log: + return renderLog(r, v) + } + return item + }) + { + v := api.BaseResponse[any]{ + Cursor: &renderedCursor, + } + s, err := json.Marshal(v) + if err != nil { + return err + } + var fields map[string]any + err = json.Unmarshal(s, &fields) + if err != nil { + return err + } + fields["resource"] = resource + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(fields); err != nil { + panic(err) + } + } + return nil +} diff --git a/internal/api/v2/controllers_queries_run_test.go b/internal/api/v2/controllers_queries_run_test.go new file mode 100644 index 000000000..d05e531c4 --- /dev/null +++ b/internal/api/v2/controllers_queries_run_test.go @@ -0,0 +1,89 @@ +package v2 + +import ( + "bytes" + "encoding/json" + "math/big" + "net/http" + "net/http/httptest" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/formancehq/go-libs/v3/auth" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/queries" + storagecommon "github.com/formancehq/ledger/internal/storage/common" +) + +func TestQueriesRun(t *testing.T) { + t.Parallel() + + systemController, ledgerController := newTestingSystemController(t, true) + router := NewRouter(systemController, auth.NewNoAuth(), "develop") + + expectedResourceKind := queries.ResourceKindTransaction + expectedCursor := bunpaginate.Cursor[any]{ + Data: []any{ + ledger.NewTransaction().WithPostings( + ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), + ), + }, + } + + expectedResponse, err := json.Marshal(map[string]any{ + "resource": "transactions", + "cursor": map[string]any{ + "data": []map[string]any{ + { + "id": nil, + "insertedAt": "0001-01-01T00:00:00Z", + "metadata": map[string]any{}, + "postings": []map[string]any{ + {"amount": 100, "asset": "USD", "destination": "bank", "source": "world"}, + }, + "reverted": false, + "timestamp": "0001-01-01T00:00:00Z", + "updatedAt": "0001-01-01T00:00:00Z", + }, + }, + "hasMore": false, + }, + }) + require.NoError(t, err) + + ledgerController.EXPECT(). + RunQuery(gomock.Any(), "1.2.3", "QUERY_ID", storagecommon.RunQuery{ + Params: json.RawMessage(`{ "pageSize": 42 }`), + Vars: map[string]any{ + "foo": float64(123.0), + "bar": "barnacle", + }, + }, storagecommon.PaginationConfig{ + MaxPageSize: bunpaginate.MaxPageSize, + DefaultPageSize: bunpaginate.QueryDefaultPageSize, + }). + Return(&expectedResourceKind, &expectedCursor, nil) + + req := httptest.NewRequest(http.MethodPost, "/xxx/queries/QUERY_ID/run?schemaVersion=1.2.3", bytes.NewBufferString(`{ + "params": { "pageSize": 42 }, + "vars": { + "foo": 123, + "bar": "barnacle" + } + }`)) + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + b := []byte{} + _, _ = rec.Body.Read(b) + spew.Dump(string(b)) + require.Equal(t, http.StatusOK, rec.Code) + + require.JSONEq(t, rec.Body.String(), string(expectedResponse)) +} diff --git a/internal/api/v2/controllers_schema_list.go b/internal/api/v2/controllers_schema_list.go index fb26fa449..51ff4841b 100644 --- a/internal/api/v2/controllers_schema_list.go +++ b/internal/api/v2/controllers_schema_list.go @@ -8,9 +8,10 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) -func listSchemas(paginationConfig common.PaginationConfig) http.HandlerFunc { +func listSchemas(paginationConfig storagecommon.PaginationConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) diff --git a/internal/api/v2/controllers_transactions_list.go b/internal/api/v2/controllers_transactions_list.go index 63efb947a..66b5b85c3 100644 --- a/internal/api/v2/controllers_transactions_list.go +++ b/internal/api/v2/controllers_transactions_list.go @@ -8,9 +8,10 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) -func listTransactions(paginationConfig common.PaginationConfig) http.HandlerFunc { +func listTransactions(paginationConfig storagecommon.PaginationConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) diff --git a/internal/api/v2/controllers_volumes.go b/internal/api/v2/controllers_volumes.go index 76601e068..163558307 100644 --- a/internal/api/v2/controllers_volumes.go +++ b/internal/api/v2/controllers_volumes.go @@ -11,10 +11,9 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" storagecommon "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ) -func readVolumes(paginationConfig common.PaginationConfig) http.HandlerFunc { +func readVolumes(paginationConfig storagecommon.PaginationConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) @@ -51,12 +50,12 @@ func readVolumes(paginationConfig common.PaginationConfig) http.HandlerFunc { } } - rq, err := getPaginatedQuery[ledgerstore.GetVolumesOptions]( + rq, err := getPaginatedQuery[ledger.GetVolumesOptions]( r, paginationConfig, "account", bunpaginate.OrderAsc, - func(rq *storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]) { + func(rq *storagecommon.ResourceQuery[ledger.GetVolumesOptions]) { if groupBy > 0 { rq.Opts.GroupLvl = groupBy } diff --git a/internal/api/v2/controllers_volumes_test.go b/internal/api/v2/controllers_volumes_test.go index fb250755d..28c275fd6 100644 --- a/internal/api/v2/controllers_volumes_test.go +++ b/internal/api/v2/controllers_volumes_test.go @@ -21,7 +21,6 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" storagecommon "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ) func TestVolumesList(t *testing.T) { @@ -31,7 +30,7 @@ func TestVolumesList(t *testing.T) { name string queryParams url.Values body string - expectQuery storagecommon.PaginatedQuery[ledgerstore.GetVolumesOptions] + expectQuery storagecommon.PaginatedQuery[ledger.GetVolumesOptions] expectStatusCode int expectedErrorCode string } @@ -40,9 +39,9 @@ func TestVolumesList(t *testing.T) { testCases := []testCase{ { name: "basic", - expectQuery: storagecommon.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + expectQuery: storagecommon.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, - Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{ + Options: storagecommon.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &before, Expand: make([]string, 0), }, @@ -53,9 +52,9 @@ func TestVolumesList(t *testing.T) { { name: "using metadata", body: `{"$match": { "metadata[roles]": "admin" }}`, - expectQuery: storagecommon.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + expectQuery: storagecommon.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, - Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{ + Options: storagecommon.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &before, Builder: query.Match("metadata[roles]", "admin"), Expand: make([]string, 0), @@ -67,9 +66,9 @@ func TestVolumesList(t *testing.T) { { name: "using account", body: `{"$match": { "account": "foo" }}`, - expectQuery: storagecommon.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + expectQuery: storagecommon.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, - Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{ + Options: storagecommon.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &before, Builder: query.Match("account", "foo"), Expand: make([]string, 0), @@ -90,12 +89,12 @@ func TestVolumesList(t *testing.T) { "pit": []string{before.Format(time.RFC3339Nano)}, "groupBy": []string{"3"}, }, - expectQuery: storagecommon.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + expectQuery: storagecommon.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, - Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{ + Options: storagecommon.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &before, Expand: make([]string, 0), - Opts: ledgerstore.GetVolumesOptions{ + Opts: ledger.GetVolumesOptions{ GroupLvl: 3, }, }, @@ -106,9 +105,9 @@ func TestVolumesList(t *testing.T) { { name: "using exists metadata filter", body: `{"$exists": { "metadata": "foo" }}`, - expectQuery: storagecommon.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + expectQuery: storagecommon.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, - Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{ + Options: storagecommon.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &before, Builder: query.Exists("metadata", "foo"), Expand: make([]string, 0), @@ -120,9 +119,9 @@ func TestVolumesList(t *testing.T) { { name: "using balance filter", body: `{"$gte": { "balance[EUR]": 50 }}`, - expectQuery: storagecommon.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + expectQuery: storagecommon.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, - Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{ + Options: storagecommon.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &before, Builder: query.Gte("balance[EUR]", big.NewInt(50)), Expand: make([]string, 0), diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go index 47383315e..2111427dd 100644 --- a/internal/api/v2/mocks_ledger_controller_test.go +++ b/internal/api/v2/mocks_ledger_controller_test.go @@ -16,8 +16,8 @@ import ( migrations "github.com/formancehq/go-libs/v3/migrations" ledger "github.com/formancehq/ledger/internal" ledger0 "github.com/formancehq/ledger/internal/controller/ledger" + queries "github.com/formancehq/ledger/internal/queries" common "github.com/formancehq/ledger/internal/storage/common" - ledger1 "github.com/formancehq/ledger/internal/storage/ledger" bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" ) @@ -401,7 +401,7 @@ func (c *LedgerControllerGetAccountCall) DoAndReturn(f func(context.Context, com } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -428,13 +428,13 @@ func (c *LedgerControllerGetAggregatedBalancesCall) Return(arg0 ledger.BalancesB } // Do rewrite *gomock.Call.Do -func (c *LedgerControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { +func (c *LedgerControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *LedgerControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { +func (c *LedgerControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *LedgerControllerGetAggregatedBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -596,7 +596,7 @@ func (c *LedgerControllerGetTransactionCall) DoAndReturn(f func(context.Context, } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -623,13 +623,13 @@ func (c *LedgerControllerGetVolumesWithBalancesCall) Return(arg0 *bunpaginate.Cu } // Do rewrite *gomock.Call.Do -func (c *LedgerControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { +func (c *LedgerControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *LedgerControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger1.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { +func (c *LedgerControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *LedgerControllerGetVolumesWithBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -1066,6 +1066,46 @@ func (c *LedgerControllerRollbackCall) DoAndReturn(f func(context.Context) error return c } +// RunQuery mocks base method. +func (m *LedgerController) RunQuery(ctx context.Context, schemaVersion, queryId string, runQuery common.RunQuery, defaultPageSize common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunQuery", ctx, schemaVersion, queryId, runQuery, defaultPageSize) + ret0, _ := ret[0].(*queries.ResourceKind) + ret1, _ := ret[1].(*bunpaginate.Cursor[any]) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RunQuery indicates an expected call of RunQuery. +func (mr *LedgerControllerMockRecorder) RunQuery(ctx, schemaVersion, queryId, runQuery, defaultPageSize any) *LedgerControllerRunQueryCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunQuery", reflect.TypeOf((*LedgerController)(nil).RunQuery), ctx, schemaVersion, queryId, runQuery, defaultPageSize) + return &LedgerControllerRunQueryCall{Call: call} +} + +// LedgerControllerRunQueryCall wrap *gomock.Call +type LedgerControllerRunQueryCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *LedgerControllerRunQueryCall) Return(arg0 *queries.ResourceKind, arg1 *bunpaginate.Cursor[any], arg2 error) *LedgerControllerRunQueryCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *LedgerControllerRunQueryCall) Do(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *LedgerControllerRunQueryCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *LedgerControllerRunQueryCall) DoAndReturn(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *LedgerControllerRunQueryCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SaveAccountMetadata mocks base method. func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, bool, error) { m.ctrl.T.Helper() diff --git a/internal/api/v2/routes.go b/internal/api/v2/routes.go index 2a6466008..c9f5076d0 100644 --- a/internal/api/v2/routes.go +++ b/internal/api/v2/routes.go @@ -15,6 +15,7 @@ import ( "github.com/formancehq/ledger/internal/api/common" v1 "github.com/formancehq/ledger/internal/api/v1" systemcontroller "github.com/formancehq/ledger/internal/controller/system" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) // NewRouter creates a chi.Router configured with the v2 HTTP API routes for the ledger service. @@ -127,6 +128,8 @@ func NewRouter( router.Get("/aggregate/balances", readBalancesAggregated) router.Get("/volumes", readVolumes(routerOptions.paginationConfig)) + + router.Post("/queries/{id}/run", runQuery(routerOptions.paginationConfig)) }) }) }) @@ -138,7 +141,7 @@ type routerOptions struct { tracer trace.Tracer bulkerFactory bulking.BulkerFactory bulkHandlerFactories map[string]bulking.HandlerFactory - paginationConfig common.PaginationConfig + paginationConfig storagecommon.PaginationConfig exporters bool } @@ -162,7 +165,7 @@ func WithBulkerFactory(bulkerFactory bulking.BulkerFactory) RouterOption { } } -func WithPaginationConfig(paginationConfig common.PaginationConfig) RouterOption { +func WithPaginationConfig(paginationConfig storagecommon.PaginationConfig) RouterOption { return func(ro *routerOptions) { ro.paginationConfig = paginationConfig } @@ -186,7 +189,7 @@ var defaultRouterOptions = []RouterOption{ WithTracer(nooptracer.Tracer{}), WithBulkerFactory(bulking.NewDefaultBulkerFactory()), WithDefaultBulkHandlerFactories(100), - WithPaginationConfig(common.PaginationConfig{ + WithPaginationConfig(storagecommon.PaginationConfig{ DefaultPageSize: bunpaginate.QueryDefaultPageSize, MaxPageSize: bunpaginate.MaxPageSize, }), diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go index 6706bd2e9..ec27cafd8 100644 --- a/internal/controller/ledger/controller.go +++ b/internal/controller/ledger/controller.go @@ -12,8 +12,8 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/machine/vm" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ) //go:generate mockgen -write_source_comment=false -typed -write_package_comment=false -source controller.go -destination controller_generated_test.go -package ledger . Controller @@ -38,8 +38,8 @@ type Controller interface { CountTransactions(ctx context.Context, query common.ResourceQuery[any]) (int, error) ListTransactions(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) GetTransaction(ctx context.Context, query common.ResourceQuery[any]) (*ledger.Transaction, error) - GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledgerstore.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) - GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) + GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) + GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) // CreateTransaction accept a numscript script and returns a transaction // It can return following errors: @@ -86,6 +86,9 @@ type Controller interface { GetSchema(ctx context.Context, version string) (*ledger.Schema, error) // ListSchemas List all schemas for the ledger ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) + + // Run a query template on the ledger + RunQuery(ctx context.Context, schemaVersion string, queryId string, runQuery common.RunQuery, defaultPageSize common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) } type RunScript = vm.RunScript diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index ef5a3905b..818cb6d34 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -22,10 +22,12 @@ import ( "github.com/formancehq/go-libs/v3/migrations" "github.com/formancehq/go-libs/v3/platform/postgres" "github.com/formancehq/go-libs/v3/pointer" + "github.com/formancehq/go-libs/v3/query" "github.com/formancehq/go-libs/v3/time" ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/machine" + "github.com/formancehq/ledger/internal/queries" storagecommon "github.com/formancehq/ledger/internal/storage/common" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" "github.com/formancehq/ledger/internal/tracing" @@ -210,7 +212,7 @@ func (ctrl *DefaultController) GetAccount(ctx context.Context, q storagecommon.R return ctrl.store.Accounts().GetOne(ctx, q) } -func (ctrl *DefaultController) GetAggregatedBalances(ctx context.Context, q storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { +func (ctrl *DefaultController) GetAggregatedBalances(ctx context.Context, q storagecommon.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { ret, err := ctrl.store.AggregatedBalances().GetOne(ctx, q) if err != nil { return nil, err @@ -222,7 +224,7 @@ func (ctrl *DefaultController) ListLogs(ctx context.Context, q storagecommon.Pag return ctrl.store.Logs().Paginate(ctx, q) } -func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q storagecommon.PaginatedQuery[ledgerstore.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q storagecommon.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { return ctrl.store.Volumes().Paginate(ctx, q) } @@ -670,6 +672,162 @@ func (ctrl *DefaultController) DeleteAccountMetadata(ctx context.Context, parame return log, idempotencyHit, err } +func (ctrl *DefaultController) runQueryFromCursor(ctx context.Context, template ledger.QueryTemplate, q storagecommon.RunQuery) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + var result *bunpaginate.Cursor[any] + switch template.Resource { + case queries.ResourceKindTransaction: + resourceQuery, err := storagecommon.UnmarshalCursor[any](*q.Cursor) + if err != nil { + return nil, nil, err + } + r, err := ctrl.store.Transactions().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.Transaction) any { return x }) + case queries.ResourceKindAccount: + resourceQuery, err := storagecommon.UnmarshalCursor[any](*q.Cursor) + if err != nil { + return nil, nil, err + } + r, err := ctrl.store.Accounts().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.Account) any { return x }) + case queries.ResourceKindLog: + resourceQuery, err := storagecommon.UnmarshalCursor[any](*q.Cursor) + if err != nil { + return nil, nil, err + } + r, err := ctrl.store.Logs().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.Log) any { return x }) + case queries.ResourceKindVolume: + resourceQuery, err := storagecommon.UnmarshalCursor[ledger.GetVolumesOptions](*q.Cursor) + if err != nil { + return nil, nil, err + } + r, err := ctrl.store.Volumes().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.VolumesWithBalanceByAssetByAccount) any { return x }) + default: + return nil, nil, fmt.Errorf("invalid resource type: %v", template.Resource) + } + return &template.Resource, result, nil +} + +func templateParamsToQuery[Opts any](params ledger.QueryTemplateParams[Opts], builder query.Builder, paginationConfig storagecommon.PaginationConfig) storagecommon.InitialPaginatedQuery[Opts] { + if uint64(params.PageSize) > paginationConfig.MaxPageSize { + params.PageSize = uint(paginationConfig.MaxPageSize) + } + return storagecommon.InitialPaginatedQuery[Opts]{ + Options: storagecommon.ResourceQuery[Opts]{ + PIT: params.PIT, + OOT: params.OOT, + Builder: builder, + Expand: params.Expand, + Opts: params.Opts, + }, + Column: params.SortColumn, + Order: params.SortOrder, + PageSize: uint64(params.PageSize), + } +} + +func (ctrl *DefaultController) RunQuery(ctx context.Context, schemaVersion string, id string, q storagecommon.RunQuery, paginationConfig storagecommon.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + schema, err := ctrl.GetSchema(ctx, schemaVersion) + if err != nil { + return nil, nil, newErrSchemaValidationError(schemaVersion, fmt.Errorf("failed to find schema: %w", err)) + } + if template, ok := schema.Queries[id]; ok { + if q.Cursor != nil { + return ctrl.runQueryFromCursor(ctx, template, q) + } else { + var result *bunpaginate.Cursor[any] + builder, err := queries.ResolveFilterTemplate(template.Resource, template.Body, template.Vars, q.Vars) + if err != nil { + return nil, nil, err + } + switch template.Resource { + case queries.ResourceKindTransaction: + params, err := ledger.QueryTemplateParams[any]{ + PageSize: uint(paginationConfig.DefaultPageSize), + SortColumn: "id", + SortOrder: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + }.Overwrite(template.Params, q.Params) + if err != nil { + return nil, nil, err + } + resourceQuery := templateParamsToQuery(*params, builder, paginationConfig) + r, err := ctrl.store.Transactions().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.Transaction) any { return x }) + case queries.ResourceKindAccount: + params, err := ledger.QueryTemplateParams[any]{ + PageSize: uint(paginationConfig.DefaultPageSize), + SortColumn: "address", + SortOrder: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + }.Overwrite(template.Params, q.Params) + if err != nil { + return nil, nil, err + } + resourceQuery := templateParamsToQuery(*params, builder, paginationConfig) + r, err := ctrl.store.Accounts().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.Account) any { return x }) + case queries.ResourceKindLog: + params, err := ledger.QueryTemplateParams[any]{ + PageSize: uint(paginationConfig.DefaultPageSize), + SortColumn: "id", + SortOrder: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + }.Overwrite(template.Params, q.Params) + if err != nil { + return nil, nil, err + } + resourceQuery := templateParamsToQuery(*params, builder, paginationConfig) + r, err := ctrl.store.Logs().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.Log) any { return x }) + case queries.ResourceKindVolume: + params, err := ledger.QueryTemplateParams[ledger.GetVolumesOptions]{ + PageSize: uint(paginationConfig.DefaultPageSize), + SortColumn: "account", + SortOrder: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + Opts: ledger.GetVolumesOptions{ + UseInsertionDate: false, + GroupLvl: 0, + }, + }.Overwrite(template.Params, q.Params) + if err != nil { + return nil, nil, err + } + resourceQuery := templateParamsToQuery(*params, builder, paginationConfig) + r, err := ctrl.store.Volumes().Paginate(ctx, resourceQuery) + if err != nil { + return nil, nil, err + } + result = bunpaginate.MapCursor(r, func(x ledger.VolumesWithBalanceByAssetByAccount) any { return x }) + default: + return nil, nil, fmt.Errorf("invalid resource type: %v", template.Resource) + } + return &template.Resource, result, nil + } + } else { + return nil, nil, newErrSchemaValidationError(schemaVersion, fmt.Errorf("unknown query template: %s", id)) + } +} + var _ Controller = (*DefaultController)(nil) type DefaultControllerOption func(controller *DefaultController) diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go index 17b7c6554..d7485254f 100644 --- a/internal/controller/ledger/controller_default_test.go +++ b/internal/controller/ledger/controller_default_test.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "encoding/json" "math/big" "testing" @@ -19,8 +20,9 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/machine/vm" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) func TestCreateTransactionWithoutSchema(t *testing.T) { @@ -536,14 +538,14 @@ func TestGetAggregatedBalances(t *testing.T) { machineParser := NewMockNumscriptParser(ctrl) interpreterParser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() - aggregatedBalances := NewMockResource[ledger.AggregatedVolumes, ledgerstore.GetAggregatedVolumesOptions](ctrl) + aggregatedBalances := NewMockResource[ledger.AggregatedVolumes, ledger.GetAggregatedVolumesOptions](ctrl) store.EXPECT().AggregatedBalances().Return(aggregatedBalances) - aggregatedBalances.EXPECT().GetOne(gomock.Any(), common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{}). + aggregatedBalances.EXPECT().GetOne(gomock.Any(), common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{}). Return(&ledger.AggregatedVolumes{}, nil) l := NewDefaultController(ledger.Ledger{}, store, parser, machineParser, interpreterParser) - ret, err := l.GetAggregatedBalances(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{}) + ret, err := l.GetAggregatedBalances(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{}) require.NoError(t, err) require.Equal(t, ledger.BalancesByAssets{}, ret) } @@ -586,17 +588,17 @@ func TestGetVolumesWithBalances(t *testing.T) { machineParser := NewMockNumscriptParser(ctrl) interpreterParser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() - volumes := NewMockPaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledgerstore.GetVolumesOptions](ctrl) + volumes := NewMockPaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger.GetVolumesOptions](ctrl) balancesByAssets := &bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]{} store.EXPECT().Volumes().Return(volumes) - volumes.EXPECT().Paginate(gomock.Any(), common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + volumes.EXPECT().Paginate(gomock.Any(), common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), }).Return(balancesByAssets, nil) l := NewDefaultController(ledger.Ledger{}, store, parser, machineParser, interpreterParser) - ret, err := l.GetVolumesWithBalances(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ + ret, err := l.GetVolumesWithBalances(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ PageSize: bunpaginate.QueryDefaultPageSize, Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), }) @@ -604,6 +606,107 @@ func TestGetVolumesWithBalances(t *testing.T) { require.Equal(t, balancesByAssets, ret) } +func TestRunQuery(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + store := NewMockStore(ctrl) + parser := NewMockNumscriptParser(ctrl) + machineParser := NewMockNumscriptParser(ctrl) + interpreterParser := NewMockNumscriptParser(ctrl) + accounts := NewMockPaginatedResource[ledger.Account, any](ctrl) + + l := NewDefaultController(ledger.Ledger{}, store, parser, machineParser, interpreterParser) + + schemaVersion := "v1.0" + schema := ledger.Schema{ + SchemaData: ledger.SchemaData{ + Chart: ledger.ChartOfAccounts{}, + Transactions: ledger.TransactionTemplates{}, + Queries: ledger.QueryTemplates{ + "FOO": { + Description: "Foo template", + Resource: "accounts", + Vars: map[string]queries.VarDecl{ + "aaa": { + Type: queries.NewTypeString(), + Default: nil, + }, + }, + Body: json.RawMessage(`{ + "$match": { + "address": "${aaa}" + } + }`), + }, + }, + }, + Version: schemaVersion, + } + + store.EXPECT(). + BeginTX(gomock.Any(), nil). + Return(store, &bun.Tx{}, nil) + + store.EXPECT(). + InsertSchema(gomock.Any(), &schema). + Return(nil) + + store.EXPECT(). + Commit(gomock.Any()). + Return(nil) + + store.EXPECT(). + InsertLog(gomock.Any(), gomock.Cond(func(x any) bool { + return x.(*ledger.Log).Type == ledger.InsertedSchemaLogType + })). + DoAndReturn(func(_ context.Context, log *ledger.Log) any { + log.ID = pointer.For(uint64(0)) + return log + }) + + _, _, _, err := l.InsertSchema(context.Background(), Parameters[InsertSchema]{ + Input: InsertSchema{ + Version: schema.Version, + Data: schema.SchemaData, + }, + }) + require.NoError(t, err) + + store.EXPECT(). + FindSchema(gomock.Any(), "v1.0"). + Return(&schema, nil) + + store.EXPECT().Accounts().Return(accounts) + + expectedQuery, err := query.ParseJSON(`{"$match": {"address": "bbb"}}`) + require.NoError(t, err) + cursor := &bunpaginate.Cursor[ledger.Account]{} + accounts.EXPECT().Paginate(gomock.Any(), common.InitialPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Column: "address", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + Options: common.ResourceQuery[any]{ + Builder: expectedQuery, + }, + }).Return(cursor, nil) + + resource, ret, err := l.RunQuery(context.Background(), schema.Version, "FOO", common.RunQuery{ + Vars: map[string]any{ + "aaa": "bbb", + }, + }, storagecommon.PaginationConfig{ + MaxPageSize: bunpaginate.MaxPageSize, + DefaultPageSize: bunpaginate.QueryDefaultPageSize, + }) + require.NoError(t, err) + require.Equal(t, queries.ResourceKindAccount, *resource) + require.Equal(t, &bunpaginate.Cursor[any]{ + Data: []any{}, + }, ret) + +} + func TestGetMigrationsInfo(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go index dee6b8d69..243a75ce3 100644 --- a/internal/controller/ledger/controller_generated_test.go +++ b/internal/controller/ledger/controller_generated_test.go @@ -15,8 +15,8 @@ import ( bunpaginate "github.com/formancehq/go-libs/v3/bun/bunpaginate" migrations "github.com/formancehq/go-libs/v3/migrations" ledger "github.com/formancehq/ledger/internal" + queries "github.com/formancehq/ledger/internal/queries" common "github.com/formancehq/ledger/internal/storage/common" - ledger0 "github.com/formancehq/ledger/internal/storage/ledger" bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" ) @@ -400,7 +400,7 @@ func (c *MockControllerGetAccountCall) DoAndReturn(f func(context.Context, commo } // GetAggregatedBalances mocks base method. -func (m *MockController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { +func (m *MockController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -427,13 +427,13 @@ func (c *MockControllerGetAggregatedBalancesCall) Return(arg0 ledger.BalancesByA } // Do rewrite *gomock.Call.Do -func (c *MockControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *MockControllerGetAggregatedBalancesCall { +func (c *MockControllerGetAggregatedBalancesCall) Do(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *MockControllerGetAggregatedBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *MockControllerGetAggregatedBalancesCall { +func (c *MockControllerGetAggregatedBalancesCall) DoAndReturn(f func(context.Context, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)) *MockControllerGetAggregatedBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -595,7 +595,7 @@ func (c *MockControllerGetTransactionCall) DoAndReturn(f func(context.Context, c } // GetVolumesWithBalances mocks base method. -func (m *MockController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *MockController) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -622,13 +622,13 @@ func (c *MockControllerGetVolumesWithBalancesCall) Return(arg0 *bunpaginate.Curs } // Do rewrite *gomock.Call.Do -func (c *MockControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *MockControllerGetVolumesWithBalancesCall { +func (c *MockControllerGetVolumesWithBalancesCall) Do(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *MockControllerGetVolumesWithBalancesCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *MockControllerGetVolumesWithBalancesCall { +func (c *MockControllerGetVolumesWithBalancesCall) DoAndReturn(f func(context.Context, common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)) *MockControllerGetVolumesWithBalancesCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -1065,6 +1065,46 @@ func (c *MockControllerRollbackCall) DoAndReturn(f func(context.Context) error) return c } +// RunQuery mocks base method. +func (m *MockController) RunQuery(ctx context.Context, schemaVersion, queryId string, runQuery common.RunQuery, defaultPageSize common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunQuery", ctx, schemaVersion, queryId, runQuery, defaultPageSize) + ret0, _ := ret[0].(*queries.ResourceKind) + ret1, _ := ret[1].(*bunpaginate.Cursor[any]) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RunQuery indicates an expected call of RunQuery. +func (mr *MockControllerMockRecorder) RunQuery(ctx, schemaVersion, queryId, runQuery, defaultPageSize any) *MockControllerRunQueryCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunQuery", reflect.TypeOf((*MockController)(nil).RunQuery), ctx, schemaVersion, queryId, runQuery, defaultPageSize) + return &MockControllerRunQueryCall{Call: call} +} + +// MockControllerRunQueryCall wrap *gomock.Call +type MockControllerRunQueryCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockControllerRunQueryCall) Return(arg0 *queries.ResourceKind, arg1 *bunpaginate.Cursor[any], arg2 error) *MockControllerRunQueryCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockControllerRunQueryCall) Do(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *MockControllerRunQueryCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockControllerRunQueryCall) DoAndReturn(f func(context.Context, string, string, common.RunQuery, common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error)) *MockControllerRunQueryCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SaveAccountMetadata mocks base method. func (m *MockController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, bool, error) { m.ctrl.T.Helper() diff --git a/internal/controller/ledger/controller_with_too_many_client_handling.go b/internal/controller/ledger/controller_with_too_many_client_handling.go index b4071cd85..7bd534aae 100644 --- a/internal/controller/ledger/controller_with_too_many_client_handling.go +++ b/internal/controller/ledger/controller_with_too_many_client_handling.go @@ -14,7 +14,9 @@ import ( "github.com/formancehq/go-libs/v3/platform/postgres" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" + storagecommon "github.com/formancehq/ledger/internal/storage/common" ) //go:generate mockgen -write_source_comment=false -typed -write_package_comment=false -source controller_with_too_many_client_handling.go -destination controller_with_too_many_client_handling_generated_test.go -package ledger . DelayCalculator @@ -171,6 +173,20 @@ func (c *ControllerWithTooManyClientHandling) ListSchemas(ctx context.Context, q return schemas, err } +func (c *ControllerWithTooManyClientHandling) RunQuery(ctx context.Context, schemaVersion string, id string, q common.RunQuery, paginationConfig storagecommon.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + var ( + resource *queries.ResourceKind + cursor *bunpaginate.Cursor[any] + err error + ) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + resource, cursor, err = c.Controller.RunQuery(ctx, schemaVersion, id, q, paginationConfig) + return err + }) + + return resource, cursor, err +} + func (c *ControllerWithTooManyClientHandling) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, *bun.Tx, error) { ctrl, tx, err := c.Controller.BeginTX(ctx, options) if err != nil { diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go index ca071e4ca..4d206f756 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -12,8 +12,9 @@ import ( "github.com/formancehq/go-libs/v3/migrations" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" + storagecommon "github.com/formancehq/ledger/internal/storage/common" "github.com/formancehq/ledger/internal/tracing" ) @@ -47,6 +48,7 @@ type ControllerWithTraces struct { insertSchemaHistogram metric.Int64Histogram getSchemaHistogram metric.Int64Histogram listSchemasHistogram metric.Int64Histogram + runQueryHistogram metric.Int64Histogram } func (c *ControllerWithTraces) Info() ledger.Ledger { @@ -164,6 +166,10 @@ func NewControllerWithTraces(underlying Controller, tracer trace.Tracer, meter m if err != nil { panic(err) } + ret.runQueryHistogram, err = meter.Int64Histogram("controller.run_query", metric.WithUnit("ms")) + if err != nil { + panic(err) + } return ret } @@ -305,7 +311,7 @@ func (c *ControllerWithTraces) GetAccount(ctx context.Context, q common.Resource ) } -func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { +func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { return tracing.TraceWithMetric( ctx, "GetAggregatedBalances", @@ -365,7 +371,7 @@ func (c *ControllerWithTraces) IsDatabaseUpToDate(ctx context.Context) (bool, er ) } -func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledgerstore.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q common.PaginatedQuery[ledger.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { return tracing.TraceWithMetric( ctx, "GetVolumesWithBalances", @@ -584,6 +590,29 @@ func (c *ControllerWithTraces) ListSchemas(ctx context.Context, query common.Pag return schemas, nil } +func (c *ControllerWithTraces) RunQuery(ctx context.Context, schemaVersion string, id string, query common.RunQuery, paginationConfig storagecommon.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { + var ( + resource *queries.ResourceKind + cursor *bunpaginate.Cursor[any] + err error + ) + _, err = tracing.TraceWithMetric( + ctx, + "RunQuery", + c.tracer, + c.runQueryHistogram, + func(ctx context.Context) (any, error) { + resource, cursor, err = c.underlying.RunQuery(ctx, schemaVersion, id, query, paginationConfig) + return nil, err + }, + ) + if err != nil { + return nil, nil, err + } + + return resource, cursor, nil +} + func (c *ControllerWithTraces) GetStats(ctx context.Context) (Stats, error) { return tracing.TraceWithMetric( ctx, diff --git a/internal/controller/ledger/mocks_test.go b/internal/controller/ledger/mocks_test.go index 50dbd4178..7c1ab419f 100644 --- a/internal/controller/ledger/mocks_test.go +++ b/internal/controller/ledger/mocks_test.go @@ -12,6 +12,7 @@ import ( reflect "reflect" bunpaginate "github.com/formancehq/go-libs/v3/bun/bunpaginate" + queries "github.com/formancehq/ledger/internal/queries" common "github.com/formancehq/ledger/internal/storage/common" bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" @@ -142,10 +143,10 @@ func (mr *MockRepositoryHandlerMockRecorder[Opts]) ResolveFilter(query, operator } // Schema mocks base method. -func (m *MockRepositoryHandler[Opts]) Schema() common.EntitySchema { +func (m *MockRepositoryHandler[Opts]) Schema() queries.EntitySchema { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Schema") - ret0, _ := ret[0].(common.EntitySchema) + ret0, _ := ret[0].(queries.EntitySchema) return ret0 } diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go index c50cbaa02..edb8c1927 100644 --- a/internal/controller/ledger/store.go +++ b/internal/controller/ledger/store.go @@ -62,8 +62,8 @@ type Store interface { Accounts() common.PaginatedResource[ledger.Account, any] Logs() common.PaginatedResource[ledger.Log, any] Transactions() common.PaginatedResource[ledger.Transaction, any] - AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledgerstore.GetAggregatedVolumesOptions] - Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledgerstore.GetVolumesOptions] + AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledger.GetAggregatedVolumesOptions] + Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger.GetVolumesOptions] } type vmStoreAdapter struct { diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 5061e3f13..89c61cdb3 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -62,10 +62,10 @@ func (mr *MockStoreMockRecorder) Accounts() *gomock.Call { } // AggregatedBalances mocks base method. -func (m *MockStore) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledger0.GetAggregatedVolumesOptions] { +func (m *MockStore) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledger.GetAggregatedVolumesOptions] { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AggregatedBalances") - ret0, _ := ret[0].(common.Resource[ledger.AggregatedVolumes, ledger0.GetAggregatedVolumesOptions]) + ret0, _ := ret[0].(common.Resource[ledger.AggregatedVolumes, ledger.GetAggregatedVolumesOptions]) return ret0 } @@ -407,10 +407,10 @@ func (mr *MockStoreMockRecorder) UpsertAccounts(ctx any, accounts ...any) *gomoc } // Volumes mocks base method. -func (m *MockStore) Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger0.GetVolumesOptions] { +func (m *MockStore) Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger.GetVolumesOptions] { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Volumes") - ret0, _ := ret[0].(common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger0.GetVolumesOptions]) + ret0, _ := ret[0].(common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger.GetVolumesOptions]) return ret0 } diff --git a/internal/controller/system/adapters.go b/internal/controller/system/adapters.go index 4d4309376..ae2c79a5e 100644 --- a/internal/controller/system/adapters.go +++ b/internal/controller/system/adapters.go @@ -72,7 +72,7 @@ func (d *DefaultStoreAdapter) Rollback(ctx context.Context) error { return d.Store.Rollback(ctx) } -func (d *DefaultStoreAdapter) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledgerstore.GetAggregatedVolumesOptions] { +func (d *DefaultStoreAdapter) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledger.GetAggregatedVolumesOptions] { return d.AggregatedVolumes() } diff --git a/internal/storage/common/schema.go b/internal/queries/field.go similarity index 81% rename from internal/storage/common/schema.go rename to internal/queries/field.go index 5eaffbc61..f70f1b681 100644 --- a/internal/storage/common/schema.go +++ b/internal/queries/field.go @@ -1,4 +1,4 @@ -package common +package queries import ( "fmt" @@ -7,21 +7,42 @@ import ( "github.com/formancehq/go-libs/v3/time" ) -const ( - OperatorMatch = "$match" - OperatorIn = "$in" - OperatorExists = "$exists" - OperatorLike = "$like" - OperatorLT = "$lt" - OperatorGT = "$gt" - OperatorLTE = "$lte" - OperatorGTE = "$gte" -) +func FieldTypeFromString(s string) (FieldType, error) { + switch s { + case "boolean": + return NewTypeBoolean(), nil + case "date": + return NewTypeDate(), nil + case "int": + return NewTypeNumeric(), nil + case "string": + return NewTypeString(), nil + default: + return nil, fmt.Errorf("invalid type `%s`, expected one of `boolean`, `date`, `int`, `string`", s) + } +} + +func FieldTypeToString(t FieldType) string { + switch v := t.(type) { + case TypeBoolean: + return "boolean" + case TypeDate: + return "date" + case TypeMap: + return fmt.Sprintf("map[string]%s", FieldTypeToString(v.underlyingType)) + case TypeNumeric: + return "int" + case TypeString: + return "string" + default: + panic(fmt.Sprintf("unexpected queries.FieldType: %#v", t)) + } +} type FieldType interface { Operators() []string ValidateValue(operator string, value any) error - IsIndexable() bool + Index() FieldType // later we might want to pass the index as argument IsPaginated() bool } @@ -41,7 +62,7 @@ func (f Field) Paginated() Field { return f } -func (f Field) matchKey(name, key string) bool { +func (f Field) MatchKey(name, key string) bool { if key == name { return true } @@ -102,8 +123,8 @@ func (t TypeString) IsPaginated() bool { return false } -func (t TypeString) IsIndexable() bool { - return false +func (t TypeString) Index() FieldType { + return nil } func (t TypeString) Operators() []string { @@ -149,8 +170,8 @@ func (t TypeDate) IsPaginated() bool { return true } -func (t TypeDate) IsIndexable() bool { - return false +func (t TypeDate) Index() FieldType { + return nil } func (t TypeDate) Operators() []string { @@ -191,8 +212,8 @@ func (t TypeMap) IsPaginated() bool { return false } -func (t TypeMap) IsIndexable() bool { - return true +func (t TypeMap) Index() FieldType { + return t.underlyingType } func (t TypeMap) Operators() []string { @@ -217,8 +238,8 @@ func (t TypeNumeric) IsPaginated() bool { return true } -func (t TypeNumeric) IsIndexable() bool { - return false +func (t TypeNumeric) Index() FieldType { + return nil } func (t TypeNumeric) Operators() []string { @@ -253,8 +274,8 @@ func (t TypeBoolean) IsPaginated() bool { return false } -func (t TypeBoolean) IsIndexable() bool { - return false +func (t TypeBoolean) Index() FieldType { + return nil } func (t TypeBoolean) Operators() []string { diff --git a/internal/queries/filter_template.go b/internal/queries/filter_template.go new file mode 100644 index 000000000..046c75f7a --- /dev/null +++ b/internal/queries/filter_template.go @@ -0,0 +1,264 @@ +package queries + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "reflect" + "regexp" + + "github.com/formancehq/go-libs/v3/query" +) + +func unmarshalWithNumber(data []byte, v any) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + return dec.Decode(v) +} + +func ValidateFilterBody(resource ResourceKind, body json.RawMessage, varDecls map[string]VarDecl) error { + var ( + vars = map[string]FieldType{} + err error + ) + for k, v := range varDecls { + vars[k] = v.Type + } + + var filter map[string]any + if err := unmarshalWithNumber(body, &filter); err != nil { + return err + } + schema := GetResourceSchema(resource) + + builder, err := query.ParseJSON(string(body)) + if err != nil { + return err + } + if builder == nil { + return nil + } + + return builder.Walk(func(operator string, key string, value *any) error { + fieldType, err := schema.GetFieldType(key) + if err != nil { + return err + } + switch operator { + case OperatorIn: + // we expect the value to be a slice of the same type as fieldType + if values, ok := (*value).([]any); ok { + for _, v := range values { + err := validateValue(fieldType, v, vars) + if err != nil { + return err + } + } + } else { + return fmt.Errorf("expected array, got `%T`", *value) + } + case OperatorExists: + // we expect the field to be a map, and the value to match its underlying type + if m, ok := fieldType.(TypeMap); ok { + return validateValue(m.underlyingType, *value, vars) + } else { + return fmt.Errorf("$exists can only be called on a map field, got: %T", fieldType) + } + case OperatorMatch, OperatorLike, OperatorLT, OperatorGT, OperatorLTE, OperatorGTE: + return validateValue(fieldType, *value, vars) + default: + return fmt.Errorf("unexpected operator: %s", operator) + } + return nil + }) +} + +func validateValue(expectedType FieldType, value any, vars map[string]FieldType) error { + // if value is a string and we don't expect a string, + // it must be a variable placeholders that we need to validate + if valueStr, ok := value.(string); ok && (expectedType != TypeString{}) { + err := validateVarRef(expectedType, valueStr, vars) + if err != nil { + return err + } + } else { + // otherwise check that the value's type matches + err := validateValueType(expectedType, value) + if err != nil { + return err + } + } + return nil +} + +func validateVarRef(expectedType FieldType, s string, vars map[string]FieldType) error { + name, err := extractVariableName(s) + if err != nil { + return err + } + if varType, ok := vars[name]; ok { + if varType != expectedType { + return fmt.Errorf("cannot use variable `%s` as type `%s`", name, FieldTypeToString(expectedType)) + } + } else { + return fmt.Errorf("variable `%v` is not declared", name) + } + return nil +} + +// Resolve filter template using the provided vars +func ResolveFilterTemplate(resourceKind ResourceKind, body json.RawMessage, varDecls map[string]VarDecl, callVars map[string]any) (query.Builder, error) { + vars := map[string]any{} + for k, v := range varDecls { + if v.Default != nil { + vars[k] = v.Default + } + } + for k, v := range callVars { + if decl, ok := varDecls[k]; ok { + if err := validateValueType(decl.Type, v); err != nil { + return nil, err + } else { + vars[k] = v + } + } + } + + schema := GetResourceSchema(resourceKind) + + builder, err := query.ParseJSON(string(body)) + if err != nil { + return nil, err + } + + err = builder.Walk(func(operator string, key string, value *any) error { + var err error + fieldType, err := schema.GetFieldType(key) + if err != nil { + return err + } + *value, err = resolveFilter(operator, fieldType, *value, vars) + if err != nil { + return fmt.Errorf("invalid filter on key %s: %w", key, err) + } + return nil + }) + if err != nil { + return nil, err + } + + return builder, nil +} + +func resolveFilter(operator string, fieldType FieldType, value any, vars map[string]any) (any, error) { + var err error + switch operator { + case OperatorIn: + // we expect the value to be a values of the same type as fieldType + if values, ok := value.([]any); ok { + for idx := range values { + if valueStr, ok := values[idx].(string); ok { + values[idx], err = resolveValue(fieldType, valueStr, vars) + if err != nil { + return nil, err + } + } + } + return values, nil + } else { + return nil, fmt.Errorf("expected array, got: %T", value) + } + case OperatorExists: + // we expect the field to be a map, and the value to match its underlying type + if m, ok := fieldType.(TypeMap); ok { + if valueStr, ok := value.(string); ok { + value, err = resolveValue(m.underlyingType, valueStr, vars) + if err != nil { + return nil, err + } + } + } else { + return nil, fmt.Errorf("$exists can only be called on a map field, got: %T", fieldType) + } + return value, nil + case OperatorMatch, OperatorLike, OperatorLT, OperatorGT, OperatorLTE, OperatorGTE: + // we expect the field to be a map, and the value to match its underlying type + if valueStr, ok := value.(string); ok { + value, err = resolveValue(fieldType, valueStr, vars) + if err != nil { + return nil, err + } + } + return value, nil + default: + return nil, fmt.Errorf("unexpected operator: %s", operator) + } +} + +func resolveValue(fieldType FieldType, value string, vars map[string]any) (any, error) { + switch fieldType.(type) { + case TypeString: + return ReplaceVariables(value, vars) + case TypeBoolean: + value, err := extractVariable[bool](value, vars) + if err != nil { + return nil, err + } + return *value, nil + case TypeDate: + value, err := extractVariable[string](value, vars) + if err != nil { + return nil, err + } + return *value, nil + case TypeNumeric: + v, err := extractVariable[json.Number](value, vars) + if err != nil { + // fallback to float64 for now + v, err2 := extractVariable[float64](value, vars) + if err2 != nil { + return nil, err + } + bigFloat := new(big.Float).SetFloat64(*v) + bigInt, acc := bigFloat.Int(nil) + if acc != big.Exact { + return nil, fmt.Errorf("provided number should be an integer: %v", v) + } + return bigInt, nil + } + if x, ok := new(big.Int).SetString(string(*v), 10); ok { + return x, nil + } else { + return nil, fmt.Errorf("provided number should be an integer: %v", v) + } + default: + return nil, fmt.Errorf("unexpected FieldType: %#v", fieldType) + } +} + +var varRegex = regexp.MustCompile(`^\${([a-z_]+)}$`) + +func extractVariableName(s string) (string, error) { + matches := varRegex.FindStringSubmatch(s) + if len(matches) == 0 { + return "", fmt.Errorf("expected a \"${variable}\" string or a plain value, got `%s`", s) + } + return matches[1], nil +} + +func extractVariable[T any](s string, vars map[string]any) (*T, error) { + name, err := extractVariableName(s) + if err != nil { + return nil, err + } + if value, ok := vars[name]; ok { + if v, ok := value.(T); ok { + return &v, nil + } else { + return nil, fmt.Errorf("cannot use variable `%s` as type `%s`", name, reflect.TypeOf((*T)(nil)).Elem().Name()) + } + } else { + return nil, fmt.Errorf("missing variable: `%s`", name) + } +} diff --git a/internal/queries/filter_template_test.go b/internal/queries/filter_template_test.go new file mode 100644 index 000000000..f0e891c6a --- /dev/null +++ b/internal/queries/filter_template_test.go @@ -0,0 +1,333 @@ +package queries + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/go-libs/v3/query" +) + +func TestFilterTemplateValidation(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + resource ResourceKind + varDeclarations map[string]VarDecl + source string + expectedError string + } + + for _, tc := range []testCase{ + { + name: "invalid substitution syntax", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{"minimum_balance": {Type: NewTypeNumeric()}}, + source: `{"$gt": { + "balance[COIN]": "${minimum_balance}000" + }}`, + expectedError: "string or a plain value", + }, + { + name: "missing variable", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{"$gt": { + "balance[COIN]": "${doesntexist}" + }}`, + expectedError: "variable `doesntexist` is not declared", + }, + { + name: "invalid field access syntax", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "minimum_balance": {Type: NewTypeNumeric(), Default: 42}, + }, + source: `{"$gt": { + "balance[COIN][THING]": "${minimum_balance}" + }}`, + expectedError: "invalid field name", + }, + { + name: "unknown field", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{"$gt": { + "doesntexist": "test" + }}`, + expectedError: "unknown field: doesntexist", + }, + { + name: "unexpected indexing", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{"$gt": { + "address[COIN]": "test" + }}`, + expectedError: "unexpected field indexing", + }, + { + name: "missing indexing", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{"$gt": { + "balance": 42 + }}`, + expectedError: "invalid value `42` for type `map[string]int`", + }, + { + name: "wrong variable type", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "wrongtype": { + Type: NewTypeString(), + Default: "test", + }, + }, + source: `{"$gt": { + "balance[COIN]": "${wrongtype}" + }}`, + expectedError: "cannot use variable `wrongtype` as type `int`", + }, + { + name: "$in with plain value", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{"$in": { + "address": "foo" + }}`, + expectedError: "expected array, got `string`", + }, + { + name: "$exists with a non-map field", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{"$exists": { + "address": "foo" + }}`, + expectedError: "$exists can only be called on a map field", + }, + } { + err := ValidateFilterBody(tc.resource, json.RawMessage(tc.source), tc.varDeclarations) + + if tc.expectedError != "" { + require.ErrorContains(t, err, tc.expectedError, tc.name) + } else { + require.NoError(t, err, tc.name) + } + } +} + +func TestFilterTemplateResolution(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + resource ResourceKind + varDeclarations map[string]VarDecl + source string + vars map[string]any + expectedError string + expectedFilter string + } + + for _, tc := range []testCase{ + { + name: "trivial case", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{ + "$gt": { + "balance[COIN]": 42 + } + }`, + vars: map[string]any{}, + expectedFilter: `{ + "$gt": { + "balance[COIN]": 42 + } + }`, + }, + { + name: "simple int substitution", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "minimum_balance": {Type: NewTypeNumeric()}, + }, + source: `{ + "$gt": { + "balance[COIN]": "${minimum_balance}" + } + }`, + vars: map[string]any{ + "minimum_balance": json.Number("42"), + }, + expectedFilter: `{ + "$gt": { + "balance[COIN]": 42 + } + }`, + }, + { + name: "simple int substitution with float64", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "minimum_balance": {Type: NewTypeNumeric()}, + }, + source: `{ + "$gt": { + "balance[COIN]": "${minimum_balance}" + } + }`, + vars: map[string]any{ + "minimum_balance": float64(42.0), + }, + expectedFilter: `{ + "$gt": { + "balance[COIN]": 42 + } + }`, + }, + { + name: "complex", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "iban": {Type: NewTypeString()}, + "minimum_balance": {Type: NewTypeNumeric()}, + "metadata_field": { + Type: NewTypeString(), + Default: "qux", + }, + }, + source: `{ + "$and": [ + {"$match": { + "address": "banks:${iban}:" + }}, + {"$or": [ + {"$gt": { + "balance[COIN]": "${minimum_balance}" + }}, + {"$exists": { + "metadata": "${metadata_field}" + }} + ]} + ] + }`, + vars: map[string]any{ + "iban": "foo", + "minimum_balance": json.Number("1000"), + }, + expectedFilter: `{ + "$and": [ + {"$match": { + "address": "banks:foo:" + }}, + {"$or": [ + {"$gt": { + "balance[COIN]": 1000 + }}, + {"$exists": { + "metadata": "qux" + }} + ]} + ] + }`, + }, + { + name: "different types", + resource: ResourceKindTransaction, + varDeclarations: map[string]VarDecl{ + "my_boolean": {Type: NewTypeBoolean()}, + "my_int": {Type: NewTypeNumeric()}, + "my_string": {Type: NewTypeString()}, + "my_date": {Type: NewTypeDate()}, + }, + source: `{ + "$and": [ + {"$match": {"reverted": "${my_boolean}"}}, + {"$match": {"account": "prefix:${my_string}:suffix"}}, + {"$match": {"timestamp": "${my_date}"}}, + {"$match": {"id": "${my_int}"}} + ] + }`, + vars: map[string]any{ + "my_boolean": false, + "my_int": json.Number("1234"), + "my_string": "foobarbazqux", + "my_date": "2023-01-01T01:01:01Z", + }, + expectedFilter: `{ + "$and": [ + {"$match": {"reverted": false}}, + {"$match": {"account": "prefix:foobarbazqux:suffix"}}, + {"$match": {"timestamp": "2023-01-01T01:01:01Z"}}, + {"$match": {"id": 1234}} + ] + }`, + }, + { + name: "substitute in value array", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "foo": { + Type: NewTypeString(), + Default: "foovalue", + }, + }, + source: `{ + "$in": { + "address": ["${foo}", "barvalue"] + } + }`, + vars: map[string]any{}, + expectedFilter: `{ + "$in": { + "address": ["foovalue", "barvalue"] + } + }`, + }, + { + name: "variable not provided", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "foo": {Type: NewTypeNumeric()}, + }, + source: `{"$gt": { + "balance[COIN]": "${foo}" + }}`, + vars: map[string]any{}, + expectedError: "missing variable: `foo`", + }, + { + name: "wrong provided variable type", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{ + "foo": {Type: NewTypeNumeric()}, + }, + source: `{"$match": { + "address": "${foo}:nope" + }}`, + vars: map[string]any{ + "foo": "nope", + }, + expectedError: "invalid value `nope` for type `int`", + }, + } { + err := ValidateFilterBody(tc.resource, json.RawMessage(tc.source), tc.varDeclarations) + require.NoError(t, err, tc.name) + + resolved, err := ResolveFilterTemplate(tc.resource, json.RawMessage(tc.source), tc.varDeclarations, tc.vars) + if tc.expectedError == "" { + require.NoError(t, err, tc.name) + + expected, err := query.ParseJSON(tc.expectedFilter) + require.NoError(t, err, tc.name) + require.Equal(t, expected, resolved, tc.name) + } else { + require.ErrorContains(t, err, tc.expectedError, tc.name) + } + } +} diff --git a/internal/queries/resources.go b/internal/queries/resources.go new file mode 100644 index 000000000..b43896647 --- /dev/null +++ b/internal/queries/resources.go @@ -0,0 +1,96 @@ +package queries + +import ( + "fmt" +) + +type ResourceKind string + +const ( + ResourceKindTransaction ResourceKind = "transactions" + ResourceKindAccount ResourceKind = "accounts" + ResourceKindLog ResourceKind = "logs" + ResourceKindVolume ResourceKind = "volumes" +) + +var Resources []ResourceKind = []ResourceKind{ + ResourceKindTransaction, + ResourceKindAccount, + ResourceKindLog, + ResourceKindVolume, +} + +var AccountSchema EntitySchema = EntitySchema{ + Fields: map[string]Field{ + "address": NewStringField().Paginated(), + "first_usage": NewDateField().Paginated(), + "balance": NewNumericMapField(), + "metadata": NewStringMapField(), + "insertion_date": NewDateField().Paginated(), + "updated_at": NewDateField().Paginated(), + }, +} + +var AggregatedBalanceSchema EntitySchema = EntitySchema{ + Fields: map[string]Field{ + "address": NewStringField().Paginated(), + "metadata": NewStringMapField(), + }, +} + +var LogSchema EntitySchema = EntitySchema{ + Fields: map[string]Field{ + "date": NewDateField().Paginated(), + "id": NewNumericField().Paginated(), + "type": NewStringField(), + }, +} + +var SchemaSchema EntitySchema = EntitySchema{ + Fields: map[string]Field{ + "version": NewStringField().Paginated(), + "created_at": NewDateField().Paginated(), + }, +} + +var TransactionSchema EntitySchema = EntitySchema{ + Fields: map[string]Field{ + "reverted": NewBooleanField(), + "account": NewStringField(), + "source": NewStringField(), + "destination": NewStringField(), + "timestamp": NewDateField().Paginated(), + "metadata": NewStringMapField(), + "id": NewNumericField().Paginated(), + "reference": NewStringField(), + "inserted_at": NewDateField().Paginated(), + "updated_at": NewDateField().Paginated(), + "reverted_at": NewDateField().Paginated(), + }, +} + +var VolumeSchema EntitySchema = EntitySchema{ + Fields: map[string]Field{ + "address": NewStringField(). + WithAliases("account"). + Paginated(), + "balance": NewNumericMapField(), + "first_usage": NewDateField(), + "metadata": NewStringMapField(), + }, +} + +func GetResourceSchema(kind ResourceKind) EntitySchema { + switch kind { + case ResourceKindAccount: + return AccountSchema + case ResourceKindLog: + return LogSchema + case ResourceKindTransaction: + return TransactionSchema + case ResourceKindVolume: + return VolumeSchema + default: + panic(fmt.Sprintf("unexpected resources.ResourceKind: %#v", kind)) + } +} diff --git a/internal/queries/schema.go b/internal/queries/schema.go new file mode 100644 index 000000000..52e49dc37 --- /dev/null +++ b/internal/queries/schema.go @@ -0,0 +1,63 @@ +package queries + +import ( + "errors" + "fmt" + "regexp" + "slices" +) + +const ( + OperatorMatch = "$match" + OperatorIn = "$in" + OperatorExists = "$exists" + OperatorLike = "$like" + OperatorLT = "$lt" + OperatorGT = "$gt" + OperatorLTE = "$lte" + OperatorGTE = "$gte" +) + +type EntitySchema struct { + Fields map[string]Field +} + +func (s EntitySchema) GetFieldByNameOrAlias(name string) (string, *Field) { + for fieldName, field := range s.Fields { + if fieldName == name || slices.Contains(field.Aliases, name) { + return fieldName, &field + } + } + + return "", nil +} + +func (s EntitySchema) GetFieldType(access string) (FieldType, error) { + key, idx, err := parseAccess(access) + if err != nil { + return nil, err + } + _, field := s.GetFieldByNameOrAlias(key) + if field == nil { + return nil, fmt.Errorf("unknown field: %s", key) + } + fieldType := field.Type + if idx != "" { + if underlyingType := field.Type.Index(); underlyingType != nil { + fieldType = underlyingType + } else { + return nil, fmt.Errorf("unexpected field indexing: %s", access) + } + } + return fieldType, nil +} + +var accessRegex = regexp.MustCompile(`^([a-z_]+)(?:\[([a-zA-Z0-9_/]+)\])?$`) + +func parseAccess(input string) (string, string, error) { + m := accessRegex.FindStringSubmatch(input) + if m == nil { + return "", "", errors.New("invalid field name") + } + return m[1], m[2], nil +} diff --git a/internal/queries/substitution.go b/internal/queries/substitution.go new file mode 100644 index 000000000..bfd0622a3 --- /dev/null +++ b/internal/queries/substitution.go @@ -0,0 +1,219 @@ +package queries + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "strconv" + "strings" +) + +func ReplaceVariables(s string, vars map[string]any) (string, error) { + strs, varRefs, err := ParseTemplate(s) + if err != nil { + return "", err + } + var buf strings.Builder + for i := range len(strs) + 1 { + if i < len(strs) { + buf.WriteString(strs[i]) + } + if i < len(varRefs) { + if v, ok := vars[varRefs[i]]; ok { + s, err := jsonToString(v) + if err != nil { + return "", err + } + buf.WriteString(s) + } else { + return "", fmt.Errorf("missing variable: %s", varRefs[i]) + } + } + } + return buf.String(), nil +} + +func jsonToString(value any) (string, error) { + switch v := value.(type) { + // we might want to disallow this completely + case float64: + if math.Floor(v) != v { + return "", errors.New("numbers with decimals are not allowed") + } + return strconv.FormatInt(int64(v), 10), nil + case json.Number: + return string(v), nil + case string: + return v, nil + case bool: + return strconv.FormatBool(v), nil + default: + return "", fmt.Errorf("unexpected variable type: %T", value) + } + +} + +type parserState struct { + str string + index int +} + +type ParsingError struct { + state parserState + expecting string +} + +func (p parserState) newParsingError(expecting string) *ParsingError { + return &ParsingError{ + state: p, + expecting: expecting, + } +} + +var _ error = (*ParsingError)(nil) + +func (e ParsingError) Error() string { + var currentChar string + if e.state.isEOF() { + currentChar = "EOF" + } else { + currentChar = fmt.Sprintf("'%c'", e.state.peek()) + } + + return fmt.Sprintf("expected %s, got %s instead", e.expecting, currentChar) +} + +func (p parserState) isEOF() bool { + return len(p.str) <= p.index +} + +// Panics on EOF +func (p parserState) peek() byte { + return p.str[p.index] +} + +// Panics on EOF +func (p *parserState) consume() byte { + ch := p.peek() + p.index++ + return ch +} + +// Returns whether the lookahead is matched by the predicat. +// Consumes on match +func (p *parserState) tryConsuming(pred func(byte) bool) (byte, bool) { + if p.isEOF() { + return 0x0, false + } + + ch := p.peek() + if pred(ch) { + p.consume() + return ch, true + } + + return 0x0, false +} + +func (p *parserState) tryConsumingCh(lookup byte) bool { + _, ok := p.tryConsuming(func(b byte) bool { + return b == lookup + }) + + return ok +} + +// lowercase chars +func isVarHeadChar(b byte) bool { + return b >= 'a' && b <= 'z' +} + +// alphanum chars or '_' +func isVarTailChar(b byte) bool { + return isVarHeadChar(b) || (b >= '0' && b <= '9') || b == '_' +} + +// Parse and consume the var identifier until we get a non-identifier char +func (p *parserState) parseVarIdent() (string, *ParsingError) { + var sb strings.Builder + + ch, ok := p.tryConsuming(isVarHeadChar) + if !ok { + return "", p.newParsingError("a lowercase char") + } + sb.WriteByte(ch) + + for { + ch, ok := p.tryConsuming(isVarTailChar) + if !ok { + // first non-identifier char means we are outside interpolation + break + } + + sb.WriteByte(ch) + } + + return sb.String(), nil +} + +// parse the $abc syntax +// PRE: already consumed opening '${' +func (p *parserState) parseSimpleVar() (string, *ParsingError) { + + if p.tryConsumingCh('{') { + return p.parseBracketVar() + } + + return p.parseVarIdent() +} + +// parse the ${abc} syntax +// PRE: already consumed opening '${' +func (p *parserState) parseBracketVar() (string, *ParsingError) { + ident, err := p.parseVarIdent() + if err != nil { + return "", err + } + + if !p.tryConsumingCh('}') { + return "", p.newParsingError("'}'") + } + + return ident, nil +} + +func ParseTemplate(str string) ([]string, []string, *ParsingError) { + p := parserState{str: str} + + // The following state is modelled as local scope by design + var strs []string + var vars []string + currentStr := "" + + for !p.isEOF() { + b := p.consume() + + switch b { + case '$': + // TODO do we append even empty str? + strs = append(strs, currentStr) + currentStr = "" + + var_, err := p.parseSimpleVar() + if err != nil { + return nil, nil, err + } + vars = append(vars, var_) + + default: + currentStr += string(b) + } + } + + if currentStr != "" { + strs = append(strs, currentStr) + } + + return strs, vars, nil +} diff --git a/internal/queries/substitution_test.go b/internal/queries/substitution_test.go new file mode 100644 index 000000000..0b529e40f --- /dev/null +++ b/internal/queries/substitution_test.go @@ -0,0 +1,116 @@ +package queries + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNoInterp(t *testing.T) { + strs, vars, err := ParseTemplate("abc") + + require.Nil(t, err) + require.Nil(t, vars) + require.Equal(t, []string{"abc"}, strs) +} + +func TestSimpleInterpSyntax(t *testing.T) { + t.Run("simple interp", func(t *testing.T) { + strs, vars, err := ParseTemplate("abc$xy") + + require.Nil(t, err) + require.Equal(t, []string{"abc"}, strs) + require.Equal(t, []string{"xy"}, vars) + }) + + t.Run("many interp (sep by space)", func(t *testing.T) { + strs, vars, err := ParseTemplate("abc$xy d$z") + + require.Nil(t, err) + require.Equal(t, []string{"abc", " d"}, strs) + require.Equal(t, []string{"xy", "z"}, vars) + }) + + t.Run("many interp (sep by colon)", func(t *testing.T) { + strs, vars, err := ParseTemplate("abc$xy:d$z") + + require.Nil(t, err) + require.Equal(t, []string{"abc", ":d"}, strs) + require.Equal(t, []string{"xy", "z"}, vars) + }) + + t.Run("many interp (no space between)", func(t *testing.T) { + strs, vars, err := ParseTemplate("abc$xy$z") + + require.Nil(t, err) + require.Equal(t, []string{"abc", ""}, strs) // <- TODO are we ok with empty str? + require.Equal(t, []string{"xy", "z"}, vars) + }) + + t.Run("single interp", func(t *testing.T) { + strs, vars, err := ParseTemplate("$x") + + require.Nil(t, err) + require.Equal(t, []string{""}, strs) // <- TODO are we ok with empty str? + require.Equal(t, []string{"x"}, vars) + }) + + t.Run("allow every char", func(t *testing.T) { + strs, vars, err := ParseTemplate("!@?\\\n$x") + + require.Nil(t, err) + require.Equal(t, []string{"!@?\\\n"}, strs) + require.Equal(t, []string{"x"}, vars) + }) + +} + +func TestComplexInterp(t *testing.T) { + t.Run("parse complex interp", func(t *testing.T) { + strs, vars, err := ParseTemplate("abc${myvar}def") + + require.Nil(t, err) + require.Equal(t, []string{"abc", "def"}, strs) + require.Equal(t, []string{"myvar"}, vars) + + }) + + t.Run("reject nested interp", func(t *testing.T) { + _, _, err := ParseTemplate("abc${$}") + require.Error(t, err) + }) + + t.Run("reject spaces", func(t *testing.T) { + _, _, err := ParseTemplate("abc${xy z}") + require.Error(t, err) + }) + + t.Run("reject nums", func(t *testing.T) { + _, _, err := ParseTemplate("abc${42}") + require.Error(t, err) + }) +} + +func TestErrMsg(t *testing.T) { + // TODO this should be a snapshot test + t.Run("unexpected EOF", func(t *testing.T) { + _, _, err := ParseTemplate("abc${") + + require.Error(t, err) + require.Equal(t, "expected a lowercase char, got EOF instead", err.Error()) + }) + + t.Run("missing closing bracket", func(t *testing.T) { + _, _, err := ParseTemplate("abc${a!def") + + require.Error(t, err) + require.Equal(t, "expected '}', got '!' instead", err.Error()) + }) + + t.Run("invalid var head char", func(t *testing.T) { + _, _, err := ParseTemplate("abc$2") + + require.Error(t, err) + require.Equal(t, "expected a lowercase char, got '2' instead", err.Error()) + }) +} diff --git a/internal/queries/variables.go b/internal/queries/variables.go new file mode 100644 index 000000000..f31d25836 --- /dev/null +++ b/internal/queries/variables.go @@ -0,0 +1,115 @@ +package queries + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/formancehq/go-libs/v3/time" +) + +type VarDecl struct { + Type FieldType + Default any +} + +func (p *VarDecl) UnmarshalJSON(b []byte) error { + // handle plain string as type + var s string + if err := unmarshalWithNumber(b, &s); err == nil { + p.Type, err = FieldTypeFromString(s) + return err + } + // handle full object case + var a struct { + Type string `json:"type,omitempty"` + Default any `json:"default"` + } + if err := unmarshalWithNumber(b, &a); err != nil { + return err + } + var err error + p.Default = a.Default + p.Type, err = FieldTypeFromString(a.Type) + return err +} + +func (p VarDecl) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + Default any `json:"default,omitempty"` + }{ + Type: FieldTypeToString(p.Type), + Default: p.Default, + }) +} + +func ValidateVarDeclarations(vars map[string]VarDecl) error { + for name, decl := range vars { + // validate default + err := validateValueType(decl.Type, decl.Default) + if err != nil { + return fmt.Errorf("invalid default for variable `%s`: %w", name, err) + } + } + return nil +} + +func validateValueType(expectedType FieldType, v any) error { + var err error + switch expectedType.(type) { + case TypeBoolean: + err = castAndValidateValue[bool](v, nil) + case TypeDate: + err = castAndValidateValue(v, func(dateString string) error { + _, err := time.ParseTime(dateString) + if err != nil { + return err + } + return nil + }) + case TypeNumeric: + err = castAndValidateValue(v, func(n json.Number) error { + if _, ok := new(big.Int).SetString(string(n), 10); !ok { + return fmt.Errorf("number should be an integer: %v", n) + } + return nil + }) + if err != nil { + err = castAndValidateValue(v, func(f float64) error { + if !new(big.Float).SetFloat64(f).IsInt() { + return fmt.Errorf("number should be an integer: %v", f) + } + return nil + }) + } + if err != nil { + err = castAndValidateValue[*big.Int](v, nil) + } + + case TypeString: + err = castAndValidateValue[string](v, nil) + default: + err = fmt.Errorf("type cannot be constructed, you may need to specify a key with `[my_key]`") + } + if err != nil { + return fmt.Errorf("invalid value `%v` for type `%s`: %w", v, FieldTypeToString(expectedType), err) + } + return nil +} + +func castAndValidateValue[T any](value any, validate func(T) error) error { + if value == nil { + return nil + } + if v, ok := value.(T); ok { + if validate != nil { + return validate(v) + } else { + return nil + } + } else { + return errors.New("value doesn't match expected type") + } +} diff --git a/internal/queries/variables_test.go b/internal/queries/variables_test.go new file mode 100644 index 000000000..ffa5be6c7 --- /dev/null +++ b/internal/queries/variables_test.go @@ -0,0 +1,106 @@ +package queries + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVariableDeclarationMarshal(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + source string + expectedVars map[string]VarDecl + expectedRoundtrip bool + expectedError string + } + + for _, tc := range []testCase{ + { + name: "invalid json.Number integer", + source: `{ + "aaa": "string", + "bbb": "boolean", + "ccc": "int", + "ddd": "date", + "eee": { + "type": "int", + "default": 42 + } + }`, + expectedVars: map[string]VarDecl{ + "aaa": {Type: NewTypeString()}, + "bbb": {Type: NewTypeBoolean()}, + "ccc": {Type: NewTypeNumeric()}, + "ddd": {Type: NewTypeDate()}, + "eee": {Type: NewTypeNumeric(), Default: json.Number("42")}, + }, + }, + { + name: "invalid json.Number integer", + source: `{ + "foo": { + "type": "int", + "default": 42 + } + }`, + expectedVars: map[string]VarDecl{ + "foo": {Type: NewTypeNumeric(), Default: json.Number("42")}, + }, + expectedRoundtrip: true, + }, + } { + var actualVars map[string]VarDecl + err := json.Unmarshal([]byte(tc.source), &actualVars) + if tc.expectedError != "" { + require.ErrorContains(t, err, tc.expectedError, tc.name) + } else { + require.NoError(t, err, tc.name) + require.Equal(t, tc.expectedVars, actualVars, tc.name) + if tc.expectedRoundtrip { + marshalled, err := json.Marshal(actualVars) + require.NoError(t, err, tc.name) + require.JSONEq(t, tc.source, string(marshalled), tc.name) + } + } + + } +} + +func TestVariableValidaton(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + vars map[string]VarDecl + expectedError string + } + + for _, tc := range []testCase{ + { + name: "invalid json.Number integer", + vars: map[string]VarDecl{ + "foo": {Type: NewTypeNumeric(), Default: json.Number("133.7")}, + }, + expectedError: "invalid value `133.7` for type `int`", + }, + { + name: "invalid float64 integer", + vars: map[string]VarDecl{ + "foo": {Type: NewTypeNumeric(), Default: 133.7}, + }, + expectedError: "invalid value `133.7` for type `int`", + }, + } { + err := ValidateVarDeclarations(tc.vars) + + if tc.expectedError != "" { + require.ErrorContains(t, err, tc.expectedError, tc.name) + } else { + require.NoError(t, err, tc.name) + } + } +} diff --git a/internal/query_template.go b/internal/query_template.go new file mode 100644 index 000000000..432a95eec --- /dev/null +++ b/internal/query_template.go @@ -0,0 +1,149 @@ +package ledger + +import ( + "bytes" + "encoding/json" + "fmt" + "slices" + "strings" + + "github.com/iancoleman/strcase" + + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/go-libs/v3/pointer" + "github.com/formancehq/go-libs/v3/time" + + "github.com/formancehq/ledger/internal/queries" +) + +type QueryTemplates map[string]QueryTemplate + +func (t QueryTemplates) Validate() error { + for _, t := range t { + if err := t.Validate(); err != nil { + return err + } + } + return nil +} + +type QueryTemplateParams[Opts any] struct { + PIT *time.Time + OOT *time.Time + Expand []string + Opts Opts + SortColumn string + SortOrder *bunpaginate.Order + PageSize uint +} + +func (p *QueryTemplateParams[Opts]) UnmarshalJSON(b []byte) error { + var x struct { + PIT *time.Time `json:"endTime"` + OOT *time.Time `json:"startTime"` + Expand []string `json:"expand,omitempty"` + Sort string `json:"sort"` + PageSize uint `json:"pageSize"` + } + err := json.Unmarshal(b, &x) + if err != nil { + return err + } + p.PIT = x.PIT + p.OOT = x.OOT + p.Expand = x.Expand + p.PageSize = x.PageSize + + if x.Sort != "" { + parts := strings.SplitN(x.Sort, ":", 2) + p.SortColumn = strcase.ToSnake(parts[0]) + if len(parts) > 1 { + switch { + case strings.ToLower(parts[1]) == "desc": + p.SortOrder = pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)) + case strings.ToLower(parts[1]) == "asc": + p.SortOrder = pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)) + default: + return fmt.Errorf("invalid order: %s", parts[1]) + } + } + } + return nil +} + +func unmarshalWithNumber(data []byte, v any) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + return dec.Decode(v) +} + +func (q QueryTemplateParams[Opts]) Overwrite(others ...json.RawMessage) (*QueryTemplateParams[Opts], error) { + for _, other := range others { + if len(other) != 0 && !bytes.Equal(bytes.TrimSpace(other), []byte("null")) { + err := unmarshalWithNumber(other, &q) + if err != nil { + return nil, err + } + err = unmarshalWithNumber(other, &q.Opts) + if err != nil { + return nil, err + } + } + } + return &q, nil +} + +type QueryTemplate struct { + Description string `json:"description,omitempty"` + Resource queries.ResourceKind `json:"resource"` + Params json.RawMessage `json:"params,omitempty"` + Vars map[string]queries.VarDecl `json:"vars,omitempty"` + Body json.RawMessage `json:"body,omitempty"` +} + +// Validate a query template +func (q QueryTemplate) Validate() error { + // check resource validity + if !slices.Contains(queries.Resources, q.Resource) { + return fmt.Errorf("unknown resource kind: %v", q.Resource) + } + // check if the params matches the resource + if len(q.Params) > 0 { + var params QueryTemplateParams[any] + err := validateParam(q.Params, ¶ms) + if err != nil { + return fmt.Errorf("invalid params: %w", err) + } + switch q.Resource { + case queries.ResourceKindVolume: + var opts GetVolumesOptions + err = validateParam(q.Params, &opts) + } + if err != nil { + return fmt.Errorf("invalid params: %w", err) + } + } + // validate variable declarations + err := queries.ValidateVarDeclarations(q.Vars) + if err != nil { + return fmt.Errorf("failed to validate variable declarations: %w", err) + } + // validate body + if len(q.Body) > 0 { + err = queries.ValidateFilterBody(q.Resource, q.Body, q.Vars) + if err != nil { + return fmt.Errorf("failed to validate filter body: %w", err) + } + } + return nil +} + +func validateParam[Opts any](params json.RawMessage, pointer *Opts) error { + if params == nil { + return nil + } + if err := unmarshalWithNumber(params, pointer); err != nil { + return err + } + return nil +} diff --git a/internal/query_template_test.go b/internal/query_template_test.go new file mode 100644 index 000000000..4ac0e7ccc --- /dev/null +++ b/internal/query_template_test.go @@ -0,0 +1,148 @@ +package ledger + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/internal/queries" +) + +func TestQueryTemplateValidation(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + source string + expectedTemplate QueryTemplate + expectedError string + } + + for _, tc := range []testCase{ + { + name: "complex & valid", + source: `{ + "description": "complex & valid", + "resource": "accounts", + "vars": { + "iban": "string" + }, + "body": { "$match": { "address": "banks:${iban}:" } } + }`, + expectedTemplate: QueryTemplate{ + Description: "complex & valid", + Resource: queries.ResourceKindAccount, + Params: nil, + Vars: map[string]queries.VarDecl{ + "iban": { + Type: queries.NewTypeString(), + }, + }, + Body: json.RawMessage(`{ "$match": { "address": "banks:${iban}:" } }`), + }, + }, + { + name: "params", + source: `{ + "description": "complex params", + "resource": "volumes", + "params": {"pageSize": 42, "groupLvl": 2} + }`, + expectedTemplate: QueryTemplate{ + Description: "complex params", + Resource: queries.ResourceKindVolume, + Params: json.RawMessage(`{"pageSize": 42, "groupLvl": 2}`), + Vars: nil, + Body: nil, + }, + }, + { + source: `{ + "description": "$in filter", + "resource": "accounts", + "vars": { + "foo": "string", + "bar": "string" + }, + "body": { + "$in": { + "metadata[foo]": ["${foo}", "${bar}"] + } + } + }`, + expectedTemplate: QueryTemplate{ + Description: "$in filter", + Resource: queries.ResourceKindAccount, + Params: nil, + Vars: map[string]queries.VarDecl{ + "foo": { + Type: queries.NewTypeString(), + }, + "bar": { + Type: queries.NewTypeString(), + }, + }, + Body: json.RawMessage(`{ + "$in": { + "metadata[foo]": ["${foo}", "${bar}"] + } + }`), + }, + }, + { + source: `{ + "description": "unknown resource kind", + "resource": "doesntexist" + }`, + expectedError: "unknown resource kind", + }, + { + name: "invalid common params", + source: `{ + "resource": "volumes", + "params": { + "sort": {} + } + }`, + expectedError: "cannot unmarshal", + }, + { + name: "invalid resource-specific params", + source: `{ + "resource": "volumes", + "params": { + "groupLvl": false + } + }`, + expectedError: "cannot unmarshal", + }, + { + name: "filter validation error", + source: `{ + "resource": "accounts", + "vars": { + "foo": "string" + }, + "body": { + "$match": { + "balance[COIN]": "${foo}" + } + } + }`, + expectedError: "cannot use variable", + }, + } { + var template QueryTemplate + err := unmarshalWithNumber([]byte(tc.source), &template) + require.NoError(t, err) + + err = template.Validate() + if tc.expectedError == "" { + require.NoError(t, err, tc.name) + require.Equal(t, tc.expectedTemplate, template, tc.name) + } else { + require.ErrorContains(t, err, tc.expectedError, tc.name) + } + } +} diff --git a/internal/resources.go b/internal/resources.go new file mode 100644 index 000000000..109597c0a --- /dev/null +++ b/internal/resources.go @@ -0,0 +1,10 @@ +package ledger + +type GetAggregatedVolumesOptions struct { + UseInsertionDate bool `json:"useInsertionDate"` +} + +type GetVolumesOptions struct { + UseInsertionDate bool `json:"useInsertionDate"` + GroupLvl int `json:"groupLvl"` +} diff --git a/internal/schema.go b/internal/schema.go index f588d6429..dd5eb7ec3 100644 --- a/internal/schema.go +++ b/internal/schema.go @@ -11,6 +11,7 @@ import ( type SchemaData struct { Chart ChartOfAccounts `json:"chart" bun:"chart"` Transactions TransactionTemplates `json:"transactions" bun:"transactions"` + Queries QueryTemplates `json:"queries" bun:"queries"` } type Schema struct { @@ -31,6 +32,9 @@ func NewSchema(version string, data SchemaData) (Schema, error) { if err := data.Transactions.Validate(); err != nil { return Schema{}, NewErrInvalidSchema(err) } + if err := data.Queries.Validate(); err != nil { + return Schema{}, NewErrInvalidSchema(err) + } return Schema{ Version: version, SchemaData: data, diff --git a/internal/storage/bucket/migrations/48-add-query-templates/notes.yaml b/internal/storage/bucket/migrations/48-add-query-templates/notes.yaml new file mode 100644 index 000000000..1c7b84e33 --- /dev/null +++ b/internal/storage/bucket/migrations/48-add-query-templates/notes.yaml @@ -0,0 +1 @@ +name: Add query templates diff --git a/internal/storage/bucket/migrations/48-add-query-templates/up.sql b/internal/storage/bucket/migrations/48-add-query-templates/up.sql new file mode 100644 index 000000000..9c6b58aad --- /dev/null +++ b/internal/storage/bucket/migrations/48-add-query-templates/up.sql @@ -0,0 +1,8 @@ +do $$ + begin + set search_path = '{{ .Schema }}'; + + alter table schemas + add column queries jsonb not null DEFAULT '{}'::jsonb; + end +$$; diff --git a/internal/storage/common/cursor.go b/internal/storage/common/cursor.go index 0728bcde8..035a7ee3f 100644 --- a/internal/storage/common/cursor.go +++ b/internal/storage/common/cursor.go @@ -18,7 +18,7 @@ func Extract[OF any]( modifiers ...func(query *InitialPaginatedQuery[OF]) error, ) (PaginatedQuery[OF], error) { if r.URL.Query().Get(bunpaginate.QueryKeyCursor) != "" { - return unmarshalCursor[OF](r.URL.Query().Get(bunpaginate.QueryKeyCursor), modifiers...) + return UnmarshalCursor[OF](r.URL.Query().Get(bunpaginate.QueryKeyCursor), modifiers...) } else { initialQuery, err := defaulter() if err != nil { @@ -28,7 +28,7 @@ func Extract[OF any]( } } -func unmarshalCursor[Options any](v string, modifiers ...func(query *InitialPaginatedQuery[Options]) error) (PaginatedQuery[Options], error) { +func UnmarshalCursor[Options any](v string, modifiers ...func(query *InitialPaginatedQuery[Options]) error) (PaginatedQuery[Options], error) { res, err := base64.RawURLEncoding.DecodeString(v) if err != nil { return nil, err @@ -92,7 +92,7 @@ func Iterate[OF any, Options any]( break } - query, err = unmarshalCursor[Options](cursor.Next) + query, err = UnmarshalCursor[Options](cursor.Next) if err != nil { return fmt.Errorf("paginating next request: %w", err) } diff --git a/internal/storage/common/pagination.go b/internal/storage/common/pagination.go index 47c28905e..318790763 100644 --- a/internal/storage/common/pagination.go +++ b/internal/storage/common/pagination.go @@ -1,11 +1,17 @@ package common import ( + "encoding/json" "math/big" "github.com/formancehq/go-libs/v3/bun/bunpaginate" ) +type PaginationConfig struct { + MaxPageSize uint64 + DefaultPageSize uint64 +} + type ( InitialPaginatedQuery[OptionsType any] struct { Column string `json:"column"` @@ -36,3 +42,27 @@ var _ PaginatedQuery[any] = (*InitialPaginatedQuery[any])(nil) var _ PaginatedQuery[any] = (*OffsetPaginatedQuery[any])(nil) var _ PaginatedQuery[any] = (*ColumnPaginatedQuery[any])(nil) + +func UnmarshalInitialPaginatedQueryOpts[TO any](from InitialPaginatedQuery[map[string]any]) (*InitialPaginatedQuery[TO], error) { + var opts TO + marshalled, err := json.Marshal(from.Options.Opts) + if err != nil { + return nil, err + } + if err := json.Unmarshal(marshalled, &opts); err != nil { + return nil, err + } else { + return &InitialPaginatedQuery[TO]{ + PageSize: from.PageSize, + Column: from.Column, + Order: from.Order, + Options: ResourceQuery[TO]{ + PIT: from.Options.PIT, + OOT: from.Options.OOT, + Builder: from.Options.Builder, + Expand: from.Options.Expand, + Opts: opts, + }, + }, nil + } +} diff --git a/internal/storage/common/paginator_column.go b/internal/storage/common/paginator_column.go index 041352ee7..15f1681f6 100644 --- a/internal/storage/common/paginator_column.go +++ b/internal/storage/common/paginator_column.go @@ -11,10 +11,12 @@ import ( "github.com/formancehq/go-libs/v3/bun/bunpaginate" "github.com/formancehq/go-libs/v3/time" + + "github.com/formancehq/ledger/internal/queries" ) type columnPaginator[ResourceType, OptionsType any] struct { - fieldType FieldType + fieldType queries.FieldType fieldName string query ColumnPaginatedQuery[OptionsType] } @@ -242,7 +244,7 @@ func findPaginationField(v any, fields ...reflect.StructField) *big.Int { func newColumnPaginator[ResourceType, OptionsType any]( query ColumnPaginatedQuery[OptionsType], fieldName string, - fieldType FieldType, + fieldType queries.FieldType, ) columnPaginator[ResourceType, OptionsType] { return columnPaginator[ResourceType, OptionsType]{ query: query, @@ -251,9 +253,9 @@ func newColumnPaginator[ResourceType, OptionsType any]( } } -func convertPaginationIDToSQLType(fieldType FieldType, id *big.Int) any { +func convertPaginationIDToSQLType(fieldType queries.FieldType, id *big.Int) any { switch fieldType.(type) { - case TypeDate: + case queries.TypeDate: return libtime.UnixMicro(id.Int64()) default: return id diff --git a/internal/storage/common/query.go b/internal/storage/common/query.go new file mode 100644 index 000000000..d3a9a53df --- /dev/null +++ b/internal/storage/common/query.go @@ -0,0 +1,9 @@ +package common + +import "encoding/json" + +type RunQuery struct { + Params json.RawMessage `json:"params,omitempty"` + Vars map[string]any `json:"vars,omitempty"` + Cursor *string `json:"cursor,omitempty"` +} diff --git a/internal/storage/common/resource.go b/internal/storage/common/resource.go index 0be267829..77d939834 100644 --- a/internal/storage/common/resource.go +++ b/internal/storage/common/resource.go @@ -14,21 +14,23 @@ import ( "github.com/formancehq/go-libs/v3/pointer" "github.com/formancehq/go-libs/v3/query" "github.com/formancehq/go-libs/v3/time" + + "github.com/formancehq/ledger/internal/queries" ) func ConvertOperatorToSQL(operator string) string { switch operator { - case OperatorMatch: + case queries.OperatorMatch: return "=" - case OperatorLT: + case queries.OperatorLT: return "<" - case OperatorGT: + case queries.OperatorGT: return ">" - case OperatorLTE: + case queries.OperatorLTE: return "<=" - case OperatorGTE: + case queries.OperatorGTE: return ">=" - case OperatorLike: + case queries.OperatorLike: return "like" } panic("unreachable") @@ -57,20 +59,6 @@ func AcceptOperators(operators ...string) PropertyValidator { }) } -type EntitySchema struct { - Fields map[string]Field -} - -func (s EntitySchema) GetFieldByNameOrAlias(name string) (string, *Field) { - for fieldName, field := range s.Fields { - if fieldName == name || slices.Contains(field.Aliases, name) { - return fieldName, &field - } - } - - return "", nil -} - type RepositoryHandlerBuildContext[Opts any] struct { ResourceQuery[Opts] filters map[string][]any @@ -102,7 +90,7 @@ func (ctx RepositoryHandlerBuildContext[Opts]) UseFilter(v string, matchers ...f } type RepositoryHandler[Opts any] interface { - Schema() EntitySchema + Schema() queries.EntitySchema BuildDataset(query RepositoryHandlerBuildContext[Opts]) (*bun.SelectQuery, error) ResolveFilter(query ResourceQuery[Opts], operator, property string, value any) (string, []any, error) Project(query ResourceQuery[Opts], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) @@ -120,14 +108,13 @@ func (r *ResourceRepository[ResourceType, OptionsType]) validateFilters(builder ret := make(map[string][]any) properties := r.resourceHandler.Schema().Fields - if err := builder.Walk(func(operator string, key string, value any) (err error) { - + if err := builder.Walk(func(operator string, key string, value *any) (err error) { for name, property := range properties { key := key - if property.Type.IsIndexable() { + if property.Type.Index() != nil { key = strings.Split(key, "[")[0] } - if !property.matchKey(name, key) { + if !property.MatchKey(name, key) { continue } @@ -135,11 +122,11 @@ func (r *ResourceRepository[ResourceType, OptionsType]) validateFilters(builder return NewErrInvalidQuery("operator '%s' is not allowed for property '%s'", operator, name) } - if err := property.Type.ValidateValue(operator, value); err != nil { - return NewErrInvalidQuery("invalid value '%v' for property '%s': %s", value, name, err) + if err := property.Type.ValidateValue(operator, *value); err != nil { + return NewErrInvalidQuery("invalid value '%v' for property '%s': %s", *value, name, err) } - ret[name] = append(ret[name], value) + ret[name] = append(ret[name], *value) return nil } diff --git a/internal/storage/ledger/balances_test.go b/internal/storage/ledger/balances_test.go index 7f1963c0c..8a483b46b 100644 --- a/internal/storage/ledger/balances_test.go +++ b/internal/storage/ledger/balances_test.go @@ -214,7 +214,7 @@ func TestBalancesAggregates(t *testing.T) { t.Run("aggregate on all", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{}) + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{}) require.NoError(t, err) RequireEqual(t, ledger.AggregatedVolumes{ Aggregated: ledger.VolumesByAssets{ @@ -238,7 +238,7 @@ func TestBalancesAggregates(t *testing.T) { t.Run("filter on address", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.Match("address", "users:"), }) require.NoError(t, err) @@ -257,7 +257,7 @@ func TestBalancesAggregates(t *testing.T) { t.Run("filter using $in on address", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.In("address", []any{"users:1", "not-existing"}), }) require.NoError(t, err) @@ -272,7 +272,7 @@ func TestBalancesAggregates(t *testing.T) { }) t.Run("using pit on effective date", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.Match("address", "users:"), PIT: pointer.For(now.Add(-time.Second)), }) @@ -291,10 +291,10 @@ func TestBalancesAggregates(t *testing.T) { }) t.Run("using pit on insertion date", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.Match("address", "users:"), PIT: pointer.For(now), - Opts: ledgerstore.GetAggregatedVolumesOptions{ + Opts: ledger.GetAggregatedVolumesOptions{ UseInsertionDate: true, }, }) @@ -313,7 +313,7 @@ func TestBalancesAggregates(t *testing.T) { }) t.Run("using a metadata and pit", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ PIT: pointer.For(now.Add(time.Minute)), Builder: query.Match("metadata[category]", "premium"), }) @@ -332,7 +332,7 @@ func TestBalancesAggregates(t *testing.T) { }) t.Run("using a metadata without pit", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.Match("metadata[category]", "premium"), }) require.NoError(t, err) @@ -348,7 +348,7 @@ func TestBalancesAggregates(t *testing.T) { }) t.Run("when no matching", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.Match("metadata[category]", "guest"), }) require.NoError(t, err) @@ -359,7 +359,7 @@ func TestBalancesAggregates(t *testing.T) { t.Run("using a filter exist on metadata", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.Exists("metadata", "category"), }) require.NoError(t, err) @@ -378,7 +378,7 @@ func TestBalancesAggregates(t *testing.T) { t.Run("using a filter on metadata and on address", func(t *testing.T) { t.Parallel() - ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ + ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ Builder: query.And( query.Match("address", "users:"), query.Match("metadata[category]", "premium"), diff --git a/internal/storage/ledger/moves_test.go b/internal/storage/ledger/moves_test.go index 97be7e453..03db4357e 100644 --- a/internal/storage/ledger/moves_test.go +++ b/internal/storage/ledger/moves_test.go @@ -20,7 +20,6 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/storage/common" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ) func TestMovesInsert(t *testing.T) { @@ -173,8 +172,8 @@ func TestMovesInsert(t *testing.T) { } wp.StopAndWait() - aggregatedVolumes, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{ - Opts: ledgerstore.GetAggregatedVolumesOptions{ + aggregatedVolumes, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledger.GetAggregatedVolumesOptions]{ + Opts: ledger.GetAggregatedVolumesOptions{ UseInsertionDate: true, }, }) diff --git a/internal/storage/ledger/queries.go b/internal/storage/ledger/queries.go index e61af5745..5d81af2ec 100644 --- a/internal/storage/ledger/queries.go +++ b/internal/storage/ledger/queries.go @@ -1,12 +1,3 @@ package ledger -type GetAggregatedVolumesOptions struct { - UseInsertionDate bool `json:"useInsertionDate"` -} - -type GetVolumesOptions struct { - UseInsertionDate bool `json:"useInsertionDate"` - GroupLvl int `json:"groupLvl"` -} - type BalanceQuery = map[string][]string diff --git a/internal/storage/ledger/resource_accounts.go b/internal/storage/ledger/resource_accounts.go index 5b3ddc329..8e0ed4727 100644 --- a/internal/storage/ledger/resource_accounts.go +++ b/internal/storage/ledger/resource_accounts.go @@ -6,6 +6,7 @@ import ( "github.com/stoewer/go-strcase" "github.com/uptrace/bun" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" "github.com/formancehq/ledger/pkg/features" ) @@ -14,17 +15,8 @@ type accountsResourceHandler struct { store *Store } -func (h accountsResourceHandler) Schema() common.EntitySchema { - return common.EntitySchema{ - Fields: map[string]common.Field{ - "address": common.NewStringField().Paginated(), - "first_usage": common.NewDateField().Paginated(), - "balance": common.NewNumericMapField(), - "metadata": common.NewStringMapField(), - "insertion_date": common.NewDateField().Paginated(), - "updated_at": common.NewDateField().Paginated(), - }, - } +func (h accountsResourceHandler) Schema() queries.EntitySchema { + return queries.AccountSchema } func (h accountsResourceHandler) BuildDataset(opts common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { @@ -63,7 +55,7 @@ func (h accountsResourceHandler) ResolveFilter(opts common.ResourceQuery[any], o fallthrough case property == "account": switch operator { - case common.OperatorIn: + case queries.OperatorIn: addresses, err := assetAddressArray(value) if err != nil { return "", nil, err diff --git a/internal/storage/ledger/resource_aggregated_balances.go b/internal/storage/ledger/resource_aggregated_balances.go index 932b2d443..1308576be 100644 --- a/internal/storage/ledger/resource_aggregated_balances.go +++ b/internal/storage/ledger/resource_aggregated_balances.go @@ -5,6 +5,8 @@ import ( "github.com/uptrace/bun" + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" "github.com/formancehq/ledger/pkg/features" ) @@ -13,16 +15,11 @@ type aggregatedBalancesResourceRepositoryHandler struct { store *Store } -func (h aggregatedBalancesResourceRepositoryHandler) Schema() common.EntitySchema { - return common.EntitySchema{ - Fields: map[string]common.Field{ - "address": common.NewStringField().Paginated(), - "metadata": common.NewStringMapField(), - }, - } +func (h aggregatedBalancesResourceRepositoryHandler) Schema() queries.EntitySchema { + return queries.AggregatedBalanceSchema } -func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.RepositoryHandlerBuildContext[GetAggregatedVolumesOptions]) (*bun.SelectQuery, error) { +func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.RepositoryHandlerBuildContext[ledger.GetAggregatedVolumesOptions]) (*bun.SelectQuery, error) { if query.UsePIT() { ret := h.store.newScopedSelect(). @@ -101,11 +98,11 @@ func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.R } } -func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.ResourceQuery[GetAggregatedVolumesOptions], operator, property string, value any) (string, []any, error) { +func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.ResourceQuery[ledger.GetAggregatedVolumesOptions], operator, property string, value any) (string, []any, error) { switch { case property == "address": switch operator { - case common.OperatorIn: + case queries.OperatorIn: addresses, err := assetAddressArray(value) if err != nil { return "", nil, err @@ -130,12 +127,12 @@ func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.Reso } } -func (h aggregatedBalancesResourceRepositoryHandler) Expand(_ common.ResourceQuery[GetAggregatedVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) { +func (h aggregatedBalancesResourceRepositoryHandler) Expand(_ common.ResourceQuery[ledger.GetAggregatedVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) { return nil, nil, errors.New("no expand available for aggregated balances") } func (h aggregatedBalancesResourceRepositoryHandler) Project( - _ common.ResourceQuery[GetAggregatedVolumesOptions], + _ common.ResourceQuery[ledger.GetAggregatedVolumesOptions], selectQuery *bun.SelectQuery, ) (*bun.SelectQuery, error) { sumVolumesForAsset := h.store.db.NewSelect(). @@ -149,4 +146,4 @@ func (h aggregatedBalancesResourceRepositoryHandler) Project( ColumnExpr("public.aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated"), nil } -var _ common.RepositoryHandler[GetAggregatedVolumesOptions] = aggregatedBalancesResourceRepositoryHandler{} +var _ common.RepositoryHandler[ledger.GetAggregatedVolumesOptions] = aggregatedBalancesResourceRepositoryHandler{} diff --git a/internal/storage/ledger/resource_logs.go b/internal/storage/ledger/resource_logs.go index 4d68e390e..67d8b8c7a 100644 --- a/internal/storage/ledger/resource_logs.go +++ b/internal/storage/ledger/resource_logs.go @@ -6,6 +6,7 @@ import ( "github.com/uptrace/bun" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" ) @@ -13,14 +14,8 @@ type logsResourceHandler struct { store *Store } -func (h logsResourceHandler) Schema() common.EntitySchema { - return common.EntitySchema{ - Fields: map[string]common.Field{ - "date": common.NewDateField().Paginated(), - "id": common.NewNumericField().Paginated(), - "type": common.NewStringField(), - }, - } +func (h logsResourceHandler) Schema() queries.EntitySchema { + return queries.LogSchema } func (h logsResourceHandler) BuildDataset(_ common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { diff --git a/internal/storage/ledger/resource_schemas.go b/internal/storage/ledger/resource_schemas.go index 5e3fd869e..68dbd8e3d 100644 --- a/internal/storage/ledger/resource_schemas.go +++ b/internal/storage/ledger/resource_schemas.go @@ -6,6 +6,7 @@ import ( "github.com/uptrace/bun" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" ) @@ -13,13 +14,8 @@ type schemasResourceHandler struct { store *Store } -func (h schemasResourceHandler) Schema() common.EntitySchema { - return common.EntitySchema{ - Fields: map[string]common.Field{ - "version": common.NewStringField().Paginated(), - "created_at": common.NewDateField().Paginated(), - }, - } +func (h schemasResourceHandler) Schema() queries.EntitySchema { + return queries.SchemaSchema } func (h schemasResourceHandler) BuildDataset(opts common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { diff --git a/internal/storage/ledger/resource_transactions.go b/internal/storage/ledger/resource_transactions.go index 6c0c51e53..f1ee9a276 100644 --- a/internal/storage/ledger/resource_transactions.go +++ b/internal/storage/ledger/resource_transactions.go @@ -6,6 +6,7 @@ import ( "github.com/uptrace/bun" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" "github.com/formancehq/ledger/pkg/features" ) @@ -14,22 +15,8 @@ type transactionsResourceHandler struct { store *Store } -func (h transactionsResourceHandler) Schema() common.EntitySchema { - return common.EntitySchema{ - Fields: map[string]common.Field{ - "reverted": common.NewBooleanField(), - "account": common.NewStringField(), - "source": common.NewStringField(), - "destination": common.NewStringField(), - "timestamp": common.NewDateField().Paginated(), - "metadata": common.NewStringMapField(), - "id": common.NewNumericField().Paginated(), - "reference": common.NewStringField(), - "inserted_at": common.NewDateField().Paginated(), - "updated_at": common.NewDateField().Paginated(), - "reverted_at": common.NewDateField().Paginated(), - }, - } +func (h transactionsResourceHandler) Schema() queries.EntitySchema { + return queries.TransactionSchema } func (h transactionsResourceHandler) BuildDataset(opts common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { @@ -92,7 +79,7 @@ func (h transactionsResourceHandler) ResolveFilter(_ common.ResourceQuery[any], return fmt.Sprintf("id %s ?", common.ConvertOperatorToSQL(operator)), []any{value}, nil case property == "reference": switch operator { - case common.OperatorIn: + case queries.OperatorIn: return "reference IN (?)", []any{bun.In(value)}, nil default: return fmt.Sprintf("reference %s ?", common.ConvertOperatorToSQL(operator)), []any{value}, nil @@ -109,7 +96,7 @@ func (h transactionsResourceHandler) ResolveFilter(_ common.ResourceQuery[any], return fmt.Sprintf("dataset.reverted_at %s ?", common.ConvertOperatorToSQL(operator)), []any{value}, nil case property == "account": switch operator { - case common.OperatorIn: + case queries.OperatorIn: addresses, err := assetAddressArray(value) if err != nil { return "", nil, err @@ -124,7 +111,7 @@ func (h transactionsResourceHandler) ResolveFilter(_ common.ResourceQuery[any], } case property == "source": switch operator { - case common.OperatorIn: + case queries.OperatorIn: addresses, err := assetAddressArray(value) if err != nil { return "", nil, err @@ -138,7 +125,7 @@ func (h transactionsResourceHandler) ResolveFilter(_ common.ResourceQuery[any], } case property == "destination": switch operator { - case common.OperatorIn: + case queries.OperatorIn: addresses, err := assetAddressArray(value) if err != nil { return "", nil, err diff --git a/internal/storage/ledger/resource_volumes.go b/internal/storage/ledger/resource_volumes.go index 72846c22b..09739b85b 100644 --- a/internal/storage/ledger/resource_volumes.go +++ b/internal/storage/ledger/resource_volumes.go @@ -7,6 +7,8 @@ import ( "github.com/uptrace/bun" + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" "github.com/formancehq/ledger/pkg/features" ) @@ -15,20 +17,11 @@ type volumesResourceHandler struct { store *Store } -func (h volumesResourceHandler) Schema() common.EntitySchema { - return common.EntitySchema{ - Fields: map[string]common.Field{ - "address": common.NewStringField(). - WithAliases("account"). - Paginated(), - "balance": common.NewNumericMapField(), - "first_usage": common.NewDateField(), - "metadata": common.NewStringMapField(), - }, - } +func (h volumesResourceHandler) Schema() queries.EntitySchema { + return queries.VolumeSchema } -func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuildContext[GetVolumesOptions]) (*bun.SelectQuery, error) { +func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuildContext[ledger.GetVolumesOptions]) (*bun.SelectQuery, error) { var selectVolumes *bun.SelectQuery @@ -128,7 +121,7 @@ func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuild } func (h volumesResourceHandler) ResolveFilter( - _ common.ResourceQuery[GetVolumesOptions], + _ common.ResourceQuery[ledger.GetVolumesOptions], operator, property string, value any, ) (string, []any, error) { @@ -136,7 +129,7 @@ func (h volumesResourceHandler) ResolveFilter( switch { case property == "address" || property == "account": switch operator { - case common.OperatorIn: + case queries.OperatorIn: addresses, err := assetAddressArray(value) if err != nil { return "", nil, err @@ -177,7 +170,7 @@ func (h volumesResourceHandler) ResolveFilter( } func (h volumesResourceHandler) Project( - query common.ResourceQuery[GetVolumesOptions], + query common.ResourceQuery[ledger.GetVolumesOptions], selectQuery *bun.SelectQuery, ) (*bun.SelectQuery, error) { selectQuery = selectQuery.DistinctOn("account, asset") @@ -200,8 +193,8 @@ func (h volumesResourceHandler) Project( GroupExpr("account, asset"), nil } -func (h volumesResourceHandler) Expand(_ common.ResourceQuery[GetVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) { +func (h volumesResourceHandler) Expand(_ common.ResourceQuery[ledger.GetVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) { return nil, nil, errors.New("no expansion available") } -var _ common.RepositoryHandler[GetVolumesOptions] = volumesResourceHandler{} +var _ common.RepositoryHandler[ledger.GetVolumesOptions] = volumesResourceHandler{} diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index faf6b1a68..10ef3a702 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -51,15 +51,15 @@ type Store struct { func (store *Store) Volumes() common.PaginatedResource[ ledger.VolumesWithBalanceByAssetByAccount, - GetVolumesOptions] { + ledger.GetVolumesOptions] { return common.NewPaginatedResourceRepository[ ledger.VolumesWithBalanceByAssetByAccount, - GetVolumesOptions, + ledger.GetVolumesOptions, ](&volumesResourceHandler{store: store}, "account", bunpaginate.OrderAsc) } -func (store *Store) AggregatedVolumes() common.Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions] { - return common.NewResourceRepository[ledger.AggregatedVolumes, GetAggregatedVolumesOptions](&aggregatedBalancesResourceRepositoryHandler{ +func (store *Store) AggregatedVolumes() common.Resource[ledger.AggregatedVolumes, ledger.GetAggregatedVolumesOptions] { + return common.NewResourceRepository[ledger.AggregatedVolumes, ledger.GetAggregatedVolumesOptions](&aggregatedBalancesResourceRepositoryHandler{ store: store, }) } diff --git a/internal/storage/ledger/volumes_test.go b/internal/storage/ledger/volumes_test.go index 132085284..0d34d5ba0 100644 --- a/internal/storage/ledger/volumes_test.go +++ b/internal/storage/ledger/volumes_test.go @@ -105,8 +105,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with first account usage filter", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ Builder: query.Lt("first_usage", now.Add(-3*time.Minute)), }, }) @@ -133,8 +133,8 @@ func TestVolumesList(t *testing.T) { }) t.Run("Get all volumes with $in on accounts", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ Builder: query.In("account", []any{"account:1", "not-existing"}), }, }) @@ -153,8 +153,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with first account usage filter and PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ Builder: query.Lt("first_usage", now.Add(-3*time.Minute)), PIT: pointer.For(now.Add(-3 * time.Minute)), }, @@ -183,9 +183,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, }, @@ -196,16 +196,16 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{}) + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{}) require.NoError(t, err) require.Len(t, volumes.Data, 4) }) t.Run("Get all volumes with balance for insertion date with previous pit", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, PIT: &previousPIT, @@ -227,9 +227,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with futur pit", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, PIT: &futurPIT, @@ -241,9 +241,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous oot", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, OOT: &previousOOT, @@ -255,9 +255,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with future oot", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, OOT: &futurOOT, @@ -279,8 +279,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with previous pit", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &previousPIT, }, }) @@ -300,8 +300,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with futur pit", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &futurPIT, }, }) @@ -311,8 +311,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with previous oot", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ OOT: &previousOOT, }, }) @@ -322,8 +322,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with futur oot", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ OOT: &futurOOT, }, }) @@ -343,9 +343,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with future PIT and now OOT", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, PIT: &futurPIT, @@ -368,9 +368,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, PIT: &now, @@ -393,8 +393,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with future PIT and now OOT", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &futurPIT, OOT: &now, }, @@ -415,8 +415,8 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &now, OOT: &previousOOT, }, @@ -439,8 +439,8 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ PIT: &now, OOT: &previousOOT, Builder: query.Match("account", "account:1"), @@ -465,8 +465,8 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ Builder: query.Match("metadata[foo]", "bar"), }, }, @@ -479,8 +479,8 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ Builder: query.Exists("metadata", "category"), }, }, @@ -493,8 +493,8 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ Builder: query.Exists("metadata", "foo"), }, }, @@ -572,9 +572,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 0", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, }, Builder: query.Match("account", "account::"), @@ -586,9 +586,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, GroupLvl: 1, }, @@ -601,9 +601,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 2", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, GroupLvl: 2, }, @@ -616,9 +616,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 3", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ UseInsertionDate: true, GroupLvl: 3, }, @@ -632,9 +632,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ GroupLvl: 1, }, PIT: &pit, @@ -667,9 +667,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate && Balance Filter 1", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ GroupLvl: 1, }, PIT: &pit, @@ -692,9 +692,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && Balance Filter 2", func(t *testing.T) { t.Parallel() - volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + volumes, err := store.Volumes().Paginate(ctx, common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ GroupLvl: 2, UseInsertionDate: true, }, @@ -737,9 +737,9 @@ func TestVolumesAggregate(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ GroupLvl: 1, }, Builder: query.And( @@ -757,9 +757,9 @@ func TestVolumesAggregate(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ GroupLvl: 1, }, PIT: pointer.For(now.Add(time.Minute)), @@ -778,9 +778,9 @@ func TestVolumesAggregate(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ - Opts: ledgerstore.GetVolumesOptions{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ + Opts: ledger.GetVolumesOptions{ GroupLvl: 1, }, Builder: query.Match("metadata[foo]", "bar"), @@ -795,8 +795,8 @@ func TestVolumesAggregate(t *testing.T) { t.Parallel() volumes, err := store.Volumes().Paginate(ctx, - common.InitialPaginatedQuery[ledgerstore.GetVolumesOptions]{ - Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{ + common.InitialPaginatedQuery[ledger.GetVolumesOptions]{ + Options: common.ResourceQuery[ledger.GetVolumesOptions]{ PIT: pointer.For(beforeBazTimestamp), Builder: query.Match("metadata[baz]", "qux"), }, diff --git a/internal/storage/system/resource_ledgers.go b/internal/storage/system/resource_ledgers.go index 74bc4e5f7..d9f8f7bf8 100644 --- a/internal/storage/system/resource_ledgers.go +++ b/internal/storage/system/resource_ledgers.go @@ -7,6 +7,7 @@ import ( "github.com/uptrace/bun" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" ) @@ -18,14 +19,14 @@ type ledgersResourceHandler struct { store *DefaultStore } -func (h ledgersResourceHandler) Schema() common.EntitySchema { - return common.EntitySchema{ - Fields: map[string]common.Field{ - "bucket": common.NewStringField(), - "features": common.NewStringMapField(), - "metadata": common.NewStringMapField(), - "name": common.NewStringField(), - "id": common.NewNumericField().Paginated(), +func (h ledgersResourceHandler) Schema() queries.EntitySchema { + return queries.EntitySchema{ + Fields: map[string]queries.Field{ + "bucket": queries.NewStringField(), + "features": queries.NewStringMapField(), + "metadata": queries.NewStringMapField(), + "name": queries.NewStringField(), + "id": queries.NewNumericField().Paginated(), }, } } diff --git a/openapi.yaml b/openapi.yaml index 3e41a7248..dda3af3f1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2843,6 +2843,120 @@ paths: security: - Authorization: - ledger:write + /v2/{ledger}/queries/{id}/run: + post: + tags: + - ledger.v2 + summary: Run a query template + description: Run a query template on a ledger + operationId: v2RunQuery + x-speakeasy-name-override: RunQuery + parameters: + - name: ledger + in: path + description: Name of the ledger. + required: true + schema: + type: string + example: ledger001 + - name: schemaVersion + in: query + required: true + description: Schema version to use for validation + schema: + type: string + example: v1.0.0 + - name: id + in: path + description: Query template ID. + required: true + schema: + type: string + example: CUSTOMER_DEPOSIT + - name: pageSize + in: query + description: | + The maximum number of results to return per page. + example: 100 + schema: + type: integer + format: int64 + minimum: 1 + maximum: 1000 + - name: cursor + in: query + description: > + Parameter used in pagination requests. Maximum page size is set to 15. + + Set to the value of next for the next page of results. + + Set to the value of previous for the previous page of results. + + No other parameters can be set when this parameter is set. + + schema: + type: string + example: aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== + - name: expand + in: query + schema: + type: string + items: + type: string + - name: pit + in: query + required: false + schema: + type: string + format: date-time + - name: order + in: query + required: false + deprecated: true + description: "Deprecated: Use sort param" + schema: + type: string + enum: + - effective + - name: reverse + in: query + required: false + schema: + type: boolean + - $ref: "#/components/parameters/sort" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + cursor: string + params: "#/components/schemas/V2QueryParams" + vars: + type: object + additionalProperties: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/V2TransactionsCursorResponse" + - $ref: "#/components/schemas/V2AccountsCursorResponse" + - $ref: "#/components/schemas/V2LogsCursorResponse" + - $ref: "#/components/schemas/V2VolumesWithBalanceCursorResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:read /v2/_/exporters: get: summary: List exporters @@ -3854,6 +3968,8 @@ components: required: - cursor properties: + resource: + const: accounts cursor: type: object required: @@ -3885,6 +4001,8 @@ components: required: - cursor properties: + resource: + const: transactions cursor: type: object required: @@ -3916,6 +4034,8 @@ components: required: - cursor properties: + resource: + const: logs cursor: type: object required: @@ -3961,6 +4081,8 @@ components: required: - cursor properties: + resource: + const: volumes cursor: type: object required: @@ -4807,6 +4929,85 @@ components: description: Transaction templates additionalProperties: $ref: "#/components/schemas/V2TransactionTemplate" + V2QueryTemplateVar: + type: object + properties: + type: string + default: string + V2QueryResource: + type: string + enum: [transactions, accounts, logs, volumes] + V2QueryParams: + type: object + properties: + pageSize: + type: integer + format: int64 + minimum: 1 + maximum: 1000 + description: | + The maximum number of results to return per page. + example: 100 + cursor: + description: > + Parameter used in pagination requests. Maximum page size is set to 15. + + Set to the value of next for the next page of results. + + Set to the value of previous for the previous page of results. + + No other parameters can be set when this parameter is set. + + type: string + example: aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== + expand: + type: string + items: + type: string + pit: + type: string + format: date-time + sort: + $ref: "#/components/parameters/sort" + oneOf: + - x-speakeasy-name-override: QueryTemplateAccountParams + properties: + resource: + const: accounts + - x-speakeasy-name-override: QueryTemplateTransactionParams + properties: + resource: + const: transactions + - x-speakeasy-name-override: QueryTemplateLogParams + properties: + resource: + const: logs + - x-speakeasy-name-override: QueryTemplateVolumeParams + properties: + resource: + const: volumes + useInsertionDate: boolean + groupLvl: integer + V2QueryTemplate: + type: object + properties: + name: string + resource: + $ref: "#/components/schemas/V2QueryResource" + params: + $ref: "#/components/schemas/V2QueryParams" + vars: + type: object + additionalProperties: + $ref: "#/components/schemas/V2QueryTemplateVar" + body: + type: object + additionalProperties: true + V2QueryTemplates: + type: object + description: Query templates + additionalProperties: + $ref: "#/components/schemas/V2QueryTemplate" V2SchemaData: type: object description: Schema data structure for ledger schemas @@ -4815,6 +5016,8 @@ components: $ref: "#/components/schemas/V2ChartOfAccounts" transactions: $ref: "#/components/schemas/V2TransactionTemplates" + queries: + $ref: "#/components/schemas/V2QueryTemplates" required: - chart - transactions diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 40bb0a56d..f4d2125bd 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -1562,6 +1562,120 @@ paths: security: - Authorization: - ledger:write + /v2/{ledger}/queries/{id}/run: + post: + tags: + - ledger.v2 + summary: Run a query template + description: Run a query template on a ledger + operationId: v2RunQuery + x-speakeasy-name-override: RunQuery + parameters: + - name: ledger + in: path + description: Name of the ledger. + required: true + schema: + type: string + example: ledger001 + - name: schemaVersion + in: query + required: true + description: Schema version to use for validation + schema: + type: string + example: v1.0.0 + - name: id + in: path + description: Query template ID. + required: true + schema: + type: string + example: CUSTOMER_DEPOSIT + - name: pageSize + in: query + description: | + The maximum number of results to return per page. + example: 100 + schema: + type: integer + format: int64 + minimum: 1 + maximum: 1000 + - name: cursor + in: query + description: > + Parameter used in pagination requests. Maximum page size is set to + 15. + + Set to the value of next for the next page of results. + + Set to the value of previous for the previous page of results. + + No other parameters can be set when this parameter is set. + schema: + type: string + example: aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== + - name: expand + in: query + schema: + type: string + items: + type: string + - name: pit + in: query + required: false + schema: + type: string + format: date-time + - name: order + in: query + required: false + deprecated: true + description: "Deprecated: Use sort param" + schema: + type: string + enum: + - effective + - name: reverse + in: query + required: false + schema: + type: boolean + - $ref: "#/components/parameters/sort" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + cursor: string + params: "#/components/schemas/V2QueryParams" + vars: + type: object + additionalProperties: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/V2TransactionsCursorResponse" + - $ref: "#/components/schemas/V2AccountsCursorResponse" + - $ref: "#/components/schemas/V2LogsCursorResponse" + - $ref: "#/components/schemas/V2VolumesWithBalanceCursorResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:read /v2/_/exporters: get: summary: List exporters @@ -2078,6 +2192,8 @@ components: required: - cursor properties: + resource: + const: accounts cursor: type: object required: @@ -2109,6 +2225,8 @@ components: required: - cursor properties: + resource: + const: transactions cursor: type: object required: @@ -2140,6 +2258,8 @@ components: required: - cursor properties: + resource: + const: logs cursor: type: object required: @@ -2185,6 +2305,8 @@ components: required: - cursor properties: + resource: + const: volumes cursor: type: object required: @@ -3031,6 +3153,85 @@ components: description: Transaction templates additionalProperties: $ref: "#/components/schemas/V2TransactionTemplate" + V2QueryTemplateVar: + type: object + properties: + type: string + default: string + V2QueryResource: + type: string + enum: [transactions, accounts, logs, volumes] + V2QueryParams: + type: object + properties: + pageSize: + type: integer + format: int64 + minimum: 1 + maximum: 1000 + description: | + The maximum number of results to return per page. + example: 100 + cursor: + description: > + Parameter used in pagination requests. Maximum page size is set to + 15. + + Set to the value of next for the next page of results. + + Set to the value of previous for the previous page of results. + + No other parameters can be set when this parameter is set. + type: string + example: aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== + expand: + type: string + items: + type: string + pit: + type: string + format: date-time + sort: + $ref: "#/components/parameters/sort" + oneOf: + - x-speakeasy-name-override: QueryTemplateAccountParams + properties: + resource: + const: accounts + - x-speakeasy-name-override: QueryTemplateTransactionParams + properties: + resource: + const: transactions + - x-speakeasy-name-override: QueryTemplateLogParams + properties: + resource: + const: logs + - x-speakeasy-name-override: QueryTemplateVolumeParams + properties: + resource: + const: volumes + useInsertionDate: boolean + groupLvl: integer + V2QueryTemplate: + type: object + properties: + name: string + resource: + $ref: "#/components/schemas/V2QueryResource" + params: + $ref: "#/components/schemas/V2QueryParams" + vars: + type: object + additionalProperties: + $ref: "#/components/schemas/V2QueryTemplateVar" + body: + type: object + additionalProperties: true + V2QueryTemplates: + type: object + description: Query templates + additionalProperties: + $ref: "#/components/schemas/V2QueryTemplate" V2SchemaData: type: object description: Schema data structure for ledger schemas @@ -3039,6 +3240,8 @@ components: $ref: "#/components/schemas/V2ChartOfAccounts" transactions: $ref: "#/components/schemas/V2TransactionTemplates" + queries: + $ref: "#/components/schemas/V2QueryTemplates" required: - chart - transactions diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index b1b6cfc4a..cd1487ef7 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 21347a677b23d73593e490cbb2a55502 + docChecksum: eb08b0100dc02c36067af2a643630e32 docVersion: v2 speakeasyVersion: 1.563.0 generationVersion: 2.629.1 @@ -120,6 +120,10 @@ generatedFiles: - /models/components/v2pipeline.go - /models/components/v2posting.go - /models/components/v2posttransaction.go + - /models/components/v2queryparams.go + - /models/components/v2queryresource.go + - /models/components/v2querytemplate.go + - /models/components/v2querytemplatevar.go - /models/components/v2reverttransactionrequest.go - /models/components/v2schema.go - /models/components/v2schemadata.go @@ -197,6 +201,7 @@ generatedFiles: - /models/operations/v2resetpipeline.go - /models/operations/v2restorebucket.go - /models/operations/v2reverttransaction.go + - /models/operations/v2runquery.go - /models/operations/v2startpipeline.go - /models/operations/v2stoppipeline.go - /models/operations/v2updateexporter.go @@ -234,6 +239,10 @@ generatedFiles: - docs/models/components/posting.md - docs/models/components/posttransaction.md - docs/models/components/posttransactionscript.md + - docs/models/components/querytemplateaccountparams.md + - docs/models/components/querytemplatelogparams.md + - docs/models/components/querytemplatetransactionparams.md + - docs/models/components/querytemplatevolumeparams.md - docs/models/components/runtime.md - docs/models/components/script.md - docs/models/components/scriptresponse.md @@ -319,6 +328,10 @@ generatedFiles: - docs/models/components/v2posting.md - docs/models/components/v2posttransaction.md - docs/models/components/v2posttransactionscript.md + - docs/models/components/v2queryparams.md + - docs/models/components/v2queryresource.md + - docs/models/components/v2querytemplate.md + - docs/models/components/v2querytemplatevar.md - docs/models/components/v2reverttransactionrequest.md - docs/models/components/v2schema.md - docs/models/components/v2schemadata.md @@ -458,6 +471,11 @@ generatedFiles: - docs/models/operations/v2restorebucketresponse.md - docs/models/operations/v2reverttransactionrequest.md - docs/models/operations/v2reverttransactionresponse.md + - docs/models/operations/v2runqueryqueryparamorder.md + - docs/models/operations/v2runqueryrequest.md + - docs/models/operations/v2runqueryrequestbody.md + - docs/models/operations/v2runqueryresponse.md + - docs/models/operations/v2runqueryresponsebody.md - docs/models/operations/v2startpipelinerequest.md - docs/models/operations/v2startpipelineresponse.md - docs/models/operations/v2stoppipelinerequest.md @@ -870,7 +888,7 @@ examples: application/json: {} responses: "200": - application/json: {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": [{"address": "users:001", "metadata": {"admin": "true"}, "volumes": {"USD": {"input": 100, "output": 10, "balance": 90}, "EUR": {"input": 100, "output": 10, "balance": 90}}, "effectiveVolumes": {"USD": {"input": 100, "output": 10, "balance": 90}, "EUR": {"input": 100, "output": 10, "balance": 90}}}]}} + application/json: {"resource": "accounts", "cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": [{"address": "users:001", "metadata": {"admin": "true"}, "volumes": {"USD": {"input": 100, "output": 10, "balance": 90}, "EUR": {"input": 100, "output": 10, "balance": 90}}, "effectiveVolumes": {"USD": {"input": 100, "output": 10, "balance": 90}, "EUR": {"input": 100, "output": 10, "balance": 90}}}]}} default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} v2GetAccount: @@ -941,7 +959,7 @@ examples: application/json: {} responses: "200": - application/json: {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": [{"timestamp": "2024-10-10T19:56:12.497Z", "postings": [], "reference": "ref:001", "metadata": {"admin": "true"}, "id": 177096, "reverted": true, "preCommitVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "postCommitVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "preCommitEffectiveVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "postCommitEffectiveVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "template": ""}]}} + application/json: {"resource": "transactions", "cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": [{"timestamp": "2024-10-10T19:56:12.497Z", "postings": [], "reference": "ref:001", "metadata": {"admin": "true"}, "id": 177096, "reverted": true, "preCommitVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "postCommitVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "preCommitEffectiveVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "postCommitEffectiveVolumes": {"orders:1": {"USD": {"input": 100, "output": 10, "balance": 90}}, "orders:2": {"USD": {"input": 100, "output": 10, "balance": 90}}}, "template": ""}]}} default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} v2CreateTransaction: @@ -1035,7 +1053,7 @@ examples: application/json: {"key": ""} responses: "200": - application/json: {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": []}} + application/json: {"resource": "volumes", "cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": []}} default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} v2ListLogs: @@ -1051,7 +1069,7 @@ examples: application/json: {} responses: "200": - application/json: {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": [{"id": 1234, "type": "NEW_TRANSACTION", "data": {"transaction": {"id": 1234, "postings": [{"amount": 100, "asset": "USD/2", "destination": "users:001", "source": "world"}], "metadata": {}, "timestamp": "2024-01-15T10:30:00Z", "insertedAt": "2024-01-15T10:30:00Z", "reverted": false}, "accountMetadata": {"users:001": {"created_by": "system"}}}, "hash": "9ee060170400f556b7e1575cb13f9db004f150a08355c7431c62bc639166431e", "date": "2023-07-05T17:46:45.404Z"}]}} + application/json: {"resource": "logs", "cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": [{"id": 1234, "type": "NEW_TRANSACTION", "data": {"transaction": {"id": 1234, "postings": [{"amount": 100, "asset": "USD/2", "destination": "users:001", "source": "world"}], "metadata": {}, "timestamp": "2024-01-15T10:30:00Z", "insertedAt": "2024-01-15T10:30:00Z", "reverted": false}, "accountMetadata": {"users:001": {"created_by": "system"}}}, "hash": "9ee060170400f556b7e1575cb13f9db004f150a08355c7431c62bc639166431e", "date": "2023-07-05T17:46:45.404Z"}]}} default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} v2ImportLogs: @@ -1243,7 +1261,7 @@ examples: ledger: "ledger001" version: "v1.0.0" requestBody: - application/json: {"chart": {"users": {"$userID": {".pattern": "^[0-9]{16}$"}}}, "transactions": {"key": {"script": ""}}} + application/json: {"chart": {"users": {"$userID": {".pattern": "^[0-9]{16}$"}}}, "transactions": {"key": {"script": ""}}, "queries": {"key": {"params": {"resource": "accounts", "pageSize": 100, "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="}}}} responses: default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} @@ -1255,7 +1273,7 @@ examples: version: "v1.0.0" responses: "200": - application/json: {"data": {"version": "v1.0.0", "createdAt": "2023-01-01T00:00:00Z", "chart": {"users": {"$userID": {".pattern": "^[0-9]{16}$"}}}, "transactions": {"key": {"script": ""}}}} + application/json: {"data": {"version": "v1.0.0", "createdAt": "2023-01-01T00:00:00Z", "chart": {"users": {"$userID": {".pattern": "^[0-9]{16}$"}}}, "transactions": {"key": {"script": ""}}, "queries": {"key": {"params": {"resource": "accounts", "pageSize": 100, "cursor": "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="}}}}} default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} v2ListSchemas: @@ -1272,5 +1290,23 @@ examples: application/json: {"cursor": {"data": [], "hasMore": true, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "pageSize": 47856}} default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} + v2RunQuery: + speakeasy-default-v2-run-query: + parameters: + path: + ledger: "ledger001" + id: "CUSTOMER_DEPOSIT" + query: + pageSize: 100 + cursor: "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==" + sort: "id:desc" + schemaVersion: "v1.0.0" + requestBody: + application/json: {} + responses: + "200": + application/json: {"resource": "transactions", "cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "aW0gdmVuaWFtLCBxdWlzIG5vc3RydWQ=", "data": []}} + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} examplesVersion: 1.0.2 generatedTests: {} diff --git a/pkg/client/.speakeasy/logs/naming.log b/pkg/client/.speakeasy/logs/naming.log index c8cc22ddf..ded8e6a26 100644 --- a/pkg/client/.speakeasy/logs/naming.log +++ b/pkg/client/.speakeasy/logs/naming.log @@ -102,13 +102,21 @@ V2CreateLedgerRequest (ledger: string, V2CreateLedgerRequest: V2CreateLedgerRequ V2CreateLedgerRequest (bucket: string, metadata: map, features: map) V2CreateLedgerResponse (HttpMeta: HTTPMetadata) V2InsertSchemaRequest (ledger: string, version: string, Idempotency-Key: string ...) - V2SchemaData (chart: map, transactions: map) + V2SchemaData (chart: map, transactions: map, queries: map) V2ChartSegment (.self: class, .pattern: string, .rules: V2ChartAccountRules ...) Self (empty) V2ChartAccountRules (empty) V2ChartAccountMetadata (default: string) V2TransactionTemplate (description: string, script: string, runtime: Runtime) Runtime (enum: experimental-interpreter, machine) + V2QueryTemplate (name: any, resource: V2QueryResource, params: V2QueryParams ...) + V2QueryResource (enum: transactions, accounts, logs ...) + V2QueryParams (union) + QueryTemplateAccountParams (resource: string, pageSize: integer, cursor: string ...) + QueryTemplateTransactionParams (resource: string, pageSize: integer, cursor: string ...) + QueryTemplateLogParams (resource: string, pageSize: integer, cursor: string ...) + QueryTemplateVolumeParams (resource: string, useInsertionDate: any, groupLvl: any ...) + V2QueryTemplateVar (type: any, default: any) V2InsertSchemaResponse (HttpMeta: HTTPMetadata, Headers: map) V2GetSchemaRequest (ledger: string, version: string) V2GetSchemaResponse (HttpMeta: HTTPMetadata, V2SchemaResponse: V2SchemaResponse) @@ -160,7 +168,7 @@ V2CountAccountsRequest (ledger: string, pit: date-time, RequestBody: map) V2CountAccountsResponse (HttpMeta: HTTPMetadata, Headers: map) V2ListAccountsRequest (ledger: string, pageSize: integer, cursor: string ...) V2ListAccountsResponse (HttpMeta: HTTPMetadata, V2AccountsCursorResponse: V2AccountsCursorResponse) - V2AccountsCursorResponse (cursor: class) + V2AccountsCursorResponse (resource: string, cursor: class) V2AccountsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2Account (address: string, metadata: map, insertionDate: date-time ...) V2GetAccountRequest (ledger: string, address: string, expand: string ...) @@ -179,7 +187,7 @@ V2CountTransactionsResponse (HttpMeta: HTTPMetadata, Headers: map) V2ListTransactionsRequest (ledger: string, pageSize: integer, cursor: string ...) QueryParamOrder (enum: effective) V2ListTransactionsResponse (HttpMeta: HTTPMetadata, V2TransactionsCursorResponse: V2TransactionsCursorResponse) - V2TransactionsCursorResponse (cursor: class) + V2TransactionsCursorResponse (resource: string, cursor: class) V2TransactionsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2CreateTransactionRequest (ledger: string, dryRun: boolean, Idempotency-Key: string ...) V2CreateTransactionResponse (HttpMeta: HTTPMetadata, V2CreateTransactionResponse: V2CreateTransactionResponse, Headers: map) @@ -199,12 +207,12 @@ V2GetBalancesAggregatedResponse (HttpMeta: HTTPMetadata, V2AggregateBalancesResp V2AggregateBalancesResponse (data: map) V2GetVolumesWithBalancesRequest (pageSize: integer, cursor: string, ledger: string ...) V2GetVolumesWithBalancesResponse (HttpMeta: HTTPMetadata, V2VolumesWithBalanceCursorResponse: V2VolumesWithBalanceCursorResponse) - V2VolumesWithBalanceCursorResponse (cursor: class) + V2VolumesWithBalanceCursorResponse (resource: string, cursor: class) V2VolumesWithBalanceCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2VolumesWithBalance (account: string, asset: string, input: bigint ...) V2ListLogsRequest (ledger: string, pageSize: integer, cursor: string ...) V2ListLogsResponse (HttpMeta: HTTPMetadata, V2LogsCursorResponse: V2LogsCursorResponse) - V2LogsCursorResponse (cursor: class) + V2LogsCursorResponse (resource: string, cursor: class) V2LogsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2Log (id: bigint, type: enum, data: union ...) V2LogType (enum: NEW_TRANSACTION, SET_METADATA, REVERTED_TRANSACTION ...) @@ -223,6 +231,11 @@ V2ImportLogsRequest (ledger: string, V2ImportLogsRequest: V2ImportLogsRequest) V2ImportLogsResponse (HttpMeta: HTTPMetadata) V2ExportLogsRequest (ledger: string) V2ExportLogsResponse (HttpMeta: HTTPMetadata) +V2RunQueryRequest (ledger: string, schemaVersion: string, id: string ...) + V2RunQueryQueryParamOrder (enum: effective) + V2RunQueryRequestBody (cursor: any, params: any, vars: map) +V2RunQueryResponse (HttpMeta: HTTPMetadata, oneOf: union) + V2RunQueryResponseBody (union) V2ListExportersResponse (HttpMeta: HTTPMetadata, V2ListExportersResponse: V2ListExportersResponse) V2ListExportersResponse (cursor: class) V2ListExportersResponseCursor (cursor: class, data: array) diff --git a/pkg/client/README.md b/pkg/client/README.md index 3451b7561..f1f579bc5 100644 --- a/pkg/client/README.md +++ b/pkg/client/README.md @@ -155,6 +155,7 @@ func main() { * [ListLogs](docs/sdks/v2/README.md#listlogs) - List the logs from a ledger * [ImportLogs](docs/sdks/v2/README.md#importlogs) * [ExportLogs](docs/sdks/v2/README.md#exportlogs) - Export logs +* [RunQuery](docs/sdks/v2/README.md#runquery) - Run a query template * [ListExporters](docs/sdks/v2/README.md#listexporters) - List exporters * [CreateExporter](docs/sdks/v2/README.md#createexporter) - Create exporter * [GetExporterState](docs/sdks/v2/README.md#getexporterstate) - Get exporter state diff --git a/pkg/client/docs/models/components/querytemplateaccountparams.md b/pkg/client/docs/models/components/querytemplateaccountparams.md new file mode 100644 index 000000000..59720c45d --- /dev/null +++ b/pkg/client/docs/models/components/querytemplateaccountparams.md @@ -0,0 +1,13 @@ +# QueryTemplateAccountParams + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | +| `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page.
| 100 | +| `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | +| `Expand` | **string* | :heavy_minus_sign: | N/A | | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `Sort` | *any* | :heavy_minus_sign: | Sort results using a field name and order (ascending or descending).
Format: `:`, where `` is the field name and `` is either `asc` or `desc`.
| | \ No newline at end of file diff --git a/pkg/client/docs/models/components/querytemplatelogparams.md b/pkg/client/docs/models/components/querytemplatelogparams.md new file mode 100644 index 000000000..f094520ea --- /dev/null +++ b/pkg/client/docs/models/components/querytemplatelogparams.md @@ -0,0 +1,13 @@ +# QueryTemplateLogParams + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | +| `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page.
| 100 | +| `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | +| `Expand` | **string* | :heavy_minus_sign: | N/A | | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `Sort` | *any* | :heavy_minus_sign: | Sort results using a field name and order (ascending or descending).
Format: `:`, where `` is the field name and `` is either `asc` or `desc`.
| | \ No newline at end of file diff --git a/pkg/client/docs/models/components/querytemplatetransactionparams.md b/pkg/client/docs/models/components/querytemplatetransactionparams.md new file mode 100644 index 000000000..0b1ba26dc --- /dev/null +++ b/pkg/client/docs/models/components/querytemplatetransactionparams.md @@ -0,0 +1,13 @@ +# QueryTemplateTransactionParams + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | +| `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page.
| 100 | +| `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | +| `Expand` | **string* | :heavy_minus_sign: | N/A | | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `Sort` | *any* | :heavy_minus_sign: | Sort results using a field name and order (ascending or descending).
Format: `:`, where `` is the field name and `` is either `asc` or `desc`.
| | \ No newline at end of file diff --git a/pkg/client/docs/models/components/querytemplatevolumeparams.md b/pkg/client/docs/models/components/querytemplatevolumeparams.md new file mode 100644 index 000000000..d0ff34869 --- /dev/null +++ b/pkg/client/docs/models/components/querytemplatevolumeparams.md @@ -0,0 +1,15 @@ +# QueryTemplateVolumeParams + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | +| `UseInsertionDate` | *any* | :heavy_minus_sign: | N/A | | +| `GroupLvl` | *any* | :heavy_minus_sign: | N/A | | +| `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page.
| 100 | +| `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | +| `Expand` | **string* | :heavy_minus_sign: | N/A | | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `Sort` | *any* | :heavy_minus_sign: | Sort results using a field name and order (ascending or descending).
Format: `:`, where `` is the field name and `` is either `asc` or `desc`.
| | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2accountscursorresponse.md b/pkg/client/docs/models/components/v2accountscursorresponse.md index feed6ea0c..1723808df 100644 --- a/pkg/client/docs/models/components/v2accountscursorresponse.md +++ b/pkg/client/docs/models/components/v2accountscursorresponse.md @@ -5,4 +5,5 @@ | Field | Type | Required | Description | | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | `Cursor` | [components.V2AccountsCursorResponseCursor](../../models/components/v2accountscursorresponsecursor.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2logscursorresponse.md b/pkg/client/docs/models/components/v2logscursorresponse.md index 8d564f753..4ed563a73 100644 --- a/pkg/client/docs/models/components/v2logscursorresponse.md +++ b/pkg/client/docs/models/components/v2logscursorresponse.md @@ -5,4 +5,5 @@ | Field | Type | Required | Description | | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | `Cursor` | [components.V2LogsCursorResponseCursor](../../models/components/v2logscursorresponsecursor.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2queryparams.md b/pkg/client/docs/models/components/v2queryparams.md new file mode 100644 index 000000000..2e3b1ea48 --- /dev/null +++ b/pkg/client/docs/models/components/v2queryparams.md @@ -0,0 +1,29 @@ +# V2QueryParams + + +## Supported Types + +### QueryTemplateAccountParams + +```go +v2QueryParams := components.CreateV2QueryParamsQueryTemplateAccountParams(components.QueryTemplateAccountParams{/* values here */}) +``` + +### QueryTemplateTransactionParams + +```go +v2QueryParams := components.CreateV2QueryParamsQueryTemplateTransactionParams(components.QueryTemplateTransactionParams{/* values here */}) +``` + +### QueryTemplateLogParams + +```go +v2QueryParams := components.CreateV2QueryParamsQueryTemplateLogParams(components.QueryTemplateLogParams{/* values here */}) +``` + +### QueryTemplateVolumeParams + +```go +v2QueryParams := components.CreateV2QueryParamsQueryTemplateVolumeParams(components.QueryTemplateVolumeParams{/* values here */}) +``` + diff --git a/pkg/client/docs/models/components/v2queryresource.md b/pkg/client/docs/models/components/v2queryresource.md new file mode 100644 index 000000000..a4286d932 --- /dev/null +++ b/pkg/client/docs/models/components/v2queryresource.md @@ -0,0 +1,11 @@ +# V2QueryResource + + +## Values + +| Name | Value | +| ----------------------------- | ----------------------------- | +| `V2QueryResourceTransactions` | transactions | +| `V2QueryResourceAccounts` | accounts | +| `V2QueryResourceLogs` | logs | +| `V2QueryResourceVolumes` | volumes | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2querytemplate.md b/pkg/client/docs/models/components/v2querytemplate.md new file mode 100644 index 000000000..262d54589 --- /dev/null +++ b/pkg/client/docs/models/components/v2querytemplate.md @@ -0,0 +1,12 @@ +# V2QueryTemplate + + +## Fields + +| Field | Type | Required | Description | +| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `Name` | *any* | :heavy_minus_sign: | N/A | +| `Resource` | [*components.V2QueryResource](../../models/components/v2queryresource.md) | :heavy_minus_sign: | N/A | +| `Params` | [*components.V2QueryParams](../../models/components/v2queryparams.md) | :heavy_minus_sign: | N/A | +| `Vars` | map[string][components.V2QueryTemplateVar](../../models/components/v2querytemplatevar.md) | :heavy_minus_sign: | N/A | +| `Body` | map[string]*any* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2querytemplatevar.md b/pkg/client/docs/models/components/v2querytemplatevar.md new file mode 100644 index 000000000..4ffdeeaf6 --- /dev/null +++ b/pkg/client/docs/models/components/v2querytemplatevar.md @@ -0,0 +1,9 @@ +# V2QueryTemplateVar + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `Type` | *any* | :heavy_minus_sign: | N/A | +| `Default` | *any* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2schema.md b/pkg/client/docs/models/components/v2schema.md index 022e5d2cf..61da813a2 100644 --- a/pkg/client/docs/models/components/v2schema.md +++ b/pkg/client/docs/models/components/v2schema.md @@ -10,4 +10,5 @@ Complete schema structure with metadata | `Version` | *string* | :heavy_check_mark: | Schema version | v1.0.0 | | `CreatedAt` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | Schema creation timestamp | 2023-01-01T00:00:00Z | | `Chart` | map[string][components.V2ChartSegment](../../models/components/v2chartsegment.md) | :heavy_check_mark: | Chart of account | {
"users": {
"$userID": {
".pattern": "^[0-9]{16}$"
}
}
} | -| `Transactions` | map[string][components.V2TransactionTemplate](../../models/components/v2transactiontemplate.md) | :heavy_check_mark: | Transaction templates | | \ No newline at end of file +| `Transactions` | map[string][components.V2TransactionTemplate](../../models/components/v2transactiontemplate.md) | :heavy_check_mark: | Transaction templates | | +| `Queries` | map[string][components.V2QueryTemplate](../../models/components/v2querytemplate.md) | :heavy_minus_sign: | Query templates | | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2schemadata.md b/pkg/client/docs/models/components/v2schemadata.md index 9f0044ccf..3b0d7d41b 100644 --- a/pkg/client/docs/models/components/v2schemadata.md +++ b/pkg/client/docs/models/components/v2schemadata.md @@ -8,4 +8,5 @@ Schema data structure for ledger schemas | Field | Type | Required | Description | Example | | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `Chart` | map[string][components.V2ChartSegment](../../models/components/v2chartsegment.md) | :heavy_check_mark: | Chart of account | {
"users": {
"$userID": {
".pattern": "^[0-9]{16}$"
}
}
} | -| `Transactions` | map[string][components.V2TransactionTemplate](../../models/components/v2transactiontemplate.md) | :heavy_check_mark: | Transaction templates | | \ No newline at end of file +| `Transactions` | map[string][components.V2TransactionTemplate](../../models/components/v2transactiontemplate.md) | :heavy_check_mark: | Transaction templates | | +| `Queries` | map[string][components.V2QueryTemplate](../../models/components/v2querytemplate.md) | :heavy_minus_sign: | Query templates | | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2transactionscursorresponse.md b/pkg/client/docs/models/components/v2transactionscursorresponse.md index 7f2b797fd..d345475c7 100644 --- a/pkg/client/docs/models/components/v2transactionscursorresponse.md +++ b/pkg/client/docs/models/components/v2transactionscursorresponse.md @@ -5,4 +5,5 @@ | Field | Type | Required | Description | | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | `Cursor` | [components.V2TransactionsCursorResponseCursor](../../models/components/v2transactionscursorresponsecursor.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2volumeswithbalancecursorresponse.md b/pkg/client/docs/models/components/v2volumeswithbalancecursorresponse.md index 29fc05293..59640d053 100644 --- a/pkg/client/docs/models/components/v2volumeswithbalancecursorresponse.md +++ b/pkg/client/docs/models/components/v2volumeswithbalancecursorresponse.md @@ -5,4 +5,5 @@ | Field | Type | Required | Description | | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `Resource` | **string* | :heavy_minus_sign: | N/A | | `Cursor` | [components.V2VolumesWithBalanceCursorResponseCursor](../../models/components/v2volumeswithbalancecursorresponsecursor.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2runqueryqueryparamorder.md b/pkg/client/docs/models/operations/v2runqueryqueryparamorder.md new file mode 100644 index 000000000..adebbce88 --- /dev/null +++ b/pkg/client/docs/models/operations/v2runqueryqueryparamorder.md @@ -0,0 +1,10 @@ +# V2RunQueryQueryParamOrder + +Deprecated: Use sort param + + +## Values + +| Name | Value | +| ------------------------------------ | ------------------------------------ | +| `V2RunQueryQueryParamOrderEffective` | effective | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2runqueryrequest.md b/pkg/client/docs/models/operations/v2runqueryrequest.md new file mode 100644 index 000000000..74cf4f0e9 --- /dev/null +++ b/pkg/client/docs/models/operations/v2runqueryrequest.md @@ -0,0 +1,18 @@ +# V2RunQueryRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `SchemaVersion` | *string* | :heavy_check_mark: | Schema version to use for validation | v1.0.0 | +| `ID` | *string* | :heavy_check_mark: | Query template ID. | CUSTOMER_DEPOSIT | +| `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page.
| 100 | +| `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | +| `Expand` | **string* | :heavy_minus_sign: | N/A | | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| ~~`Order`~~ | [*operations.V2RunQueryQueryParamOrder](../../models/operations/v2runqueryqueryparamorder.md) | :heavy_minus_sign: | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible.

Deprecated: Use sort param | | +| `Reverse` | **bool* | :heavy_minus_sign: | N/A | | +| `Sort` | **string* | :heavy_minus_sign: | Sort results using a field name and order (ascending or descending).
Format: `:`, where `` is the field name and `` is either `asc` or `desc`.
| id:desc | +| `RequestBody` | [operations.V2RunQueryRequestBody](../../models/operations/v2runqueryrequestbody.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2runqueryrequestbody.md b/pkg/client/docs/models/operations/v2runqueryrequestbody.md new file mode 100644 index 000000000..f8aa52cf5 --- /dev/null +++ b/pkg/client/docs/models/operations/v2runqueryrequestbody.md @@ -0,0 +1,10 @@ +# V2RunQueryRequestBody + + +## Fields + +| Field | Type | Required | Description | +| ------------------- | ------------------- | ------------------- | ------------------- | +| `Cursor` | *any* | :heavy_minus_sign: | N/A | +| `Params` | *any* | :heavy_minus_sign: | N/A | +| `Vars` | map[string]*string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2runqueryresponse.md b/pkg/client/docs/models/operations/v2runqueryresponse.md new file mode 100644 index 000000000..b20206c95 --- /dev/null +++ b/pkg/client/docs/models/operations/v2runqueryresponse.md @@ -0,0 +1,9 @@ +# V2RunQueryResponse + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `OneOf` | [*operations.V2RunQueryResponseBody](../../models/operations/v2runqueryresponsebody.md) | :heavy_minus_sign: | OK | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2runqueryresponsebody.md b/pkg/client/docs/models/operations/v2runqueryresponsebody.md new file mode 100644 index 000000000..2807ba592 --- /dev/null +++ b/pkg/client/docs/models/operations/v2runqueryresponsebody.md @@ -0,0 +1,31 @@ +# V2RunQueryResponseBody + +OK + + +## Supported Types + +### V2TransactionsCursorResponse + +```go +v2RunQueryResponseBody := operations.CreateV2RunQueryResponseBodyV2TransactionsCursorResponse(components.V2TransactionsCursorResponse{/* values here */}) +``` + +### V2AccountsCursorResponse + +```go +v2RunQueryResponseBody := operations.CreateV2RunQueryResponseBodyV2AccountsCursorResponse(components.V2AccountsCursorResponse{/* values here */}) +``` + +### V2LogsCursorResponse + +```go +v2RunQueryResponseBody := operations.CreateV2RunQueryResponseBodyV2LogsCursorResponse(components.V2LogsCursorResponse{/* values here */}) +``` + +### V2VolumesWithBalanceCursorResponse + +```go +v2RunQueryResponseBody := operations.CreateV2RunQueryResponseBodyV2VolumesWithBalanceCursorResponse(components.V2VolumesWithBalanceCursorResponse{/* values here */}) +``` + diff --git a/pkg/client/docs/sdks/v2/README.md b/pkg/client/docs/sdks/v2/README.md index ada8e8d3d..fc93ea885 100644 --- a/pkg/client/docs/sdks/v2/README.md +++ b/pkg/client/docs/sdks/v2/README.md @@ -33,6 +33,7 @@ * [ListLogs](#listlogs) - List the logs from a ledger * [ImportLogs](#importlogs) * [ExportLogs](#exportlogs) - Export logs +* [RunQuery](#runquery) - Run a query template * [ListExporters](#listexporters) - List exporters * [CreateExporter](#createexporter) - Create exporter * [GetExporterState](#getexporterstate) - Get exporter state @@ -283,6 +284,16 @@ func main() { Script: "", }, }, + Queries: map[string]components.V2QueryTemplate{ + "key": components.V2QueryTemplate{ + Params: client.Pointer(components.CreateV2QueryParamsQueryTemplateAccountParams( + components.QueryTemplateAccountParams{ + PageSize: client.Int64(100), + Cursor: client.String("aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="), + }, + )), + }, + }, }, }) if err != nil { @@ -1854,6 +1865,71 @@ func main() { | ------------------ | ------------------ | ------------------ | | sdkerrors.SDKError | 4XX, 5XX | \*/\* | +## RunQuery + +Run a query template on a ledger + +### Example Usage + +```go +package main + +import( + "context" + "os" + "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client" + "github.com/formancehq/ledger/pkg/client/models/operations" + "log" +) + +func main() { + ctx := context.Background() + + s := client.New( + client.WithSecurity(components.Security{ + ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")), + ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")), + }), + ) + + res, err := s.Ledger.V2.RunQuery(ctx, operations.V2RunQueryRequest{ + Ledger: "ledger001", + SchemaVersion: "v1.0.0", + ID: "CUSTOMER_DEPOSIT", + PageSize: client.Int64(100), + Cursor: client.String("aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="), + Sort: client.String("id:desc"), + RequestBody: operations.V2RunQueryRequestBody{}, + }) + if err != nil { + log.Fatal(err) + } + if res.OneOf != nil { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | +| `request` | [operations.V2RunQueryRequest](../../models/operations/v2runqueryrequest.md) | :heavy_check_mark: | The request object to use for the request. | +| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. | + +### Response + +**[*operations.V2RunQueryResponse](../../models/operations/v2runqueryresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------------- | ------------------------- | ------------------------- | +| sdkerrors.V2ErrorResponse | default | application/json | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + ## ListExporters List exporters diff --git a/pkg/client/models/components/v2accountscursorresponse.go b/pkg/client/models/components/v2accountscursorresponse.go index d8453cb36..97a260719 100644 --- a/pkg/client/models/components/v2accountscursorresponse.go +++ b/pkg/client/models/components/v2accountscursorresponse.go @@ -2,6 +2,11 @@ package components +import ( + "github.com/formancehq/ledger/pkg/client/internal/utils" + "github.com/formancehq/ledger/pkg/client/types" +) + type V2AccountsCursorResponseCursor struct { PageSize int64 `json:"pageSize"` HasMore bool `json:"hasMore"` @@ -46,7 +51,23 @@ func (o *V2AccountsCursorResponseCursor) GetData() []V2Account { } type V2AccountsCursorResponse struct { - Cursor V2AccountsCursorResponseCursor `json:"cursor"` + resource *string `const:"accounts" json:"resource,omitempty"` + Cursor V2AccountsCursorResponseCursor `json:"cursor"` +} + +func (v V2AccountsCursorResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V2AccountsCursorResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, true); err != nil { + return err + } + return nil +} + +func (o *V2AccountsCursorResponse) GetResource() *string { + return types.String("accounts") } func (o *V2AccountsCursorResponse) GetCursor() V2AccountsCursorResponseCursor { diff --git a/pkg/client/models/components/v2logscursorresponse.go b/pkg/client/models/components/v2logscursorresponse.go index 610a21619..02459722d 100644 --- a/pkg/client/models/components/v2logscursorresponse.go +++ b/pkg/client/models/components/v2logscursorresponse.go @@ -2,6 +2,11 @@ package components +import ( + "github.com/formancehq/ledger/pkg/client/internal/utils" + "github.com/formancehq/ledger/pkg/client/types" +) + type V2LogsCursorResponseCursor struct { PageSize int64 `json:"pageSize"` HasMore bool `json:"hasMore"` @@ -46,7 +51,23 @@ func (o *V2LogsCursorResponseCursor) GetData() []V2Log { } type V2LogsCursorResponse struct { - Cursor V2LogsCursorResponseCursor `json:"cursor"` + resource *string `const:"logs" json:"resource,omitempty"` + Cursor V2LogsCursorResponseCursor `json:"cursor"` +} + +func (v V2LogsCursorResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V2LogsCursorResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, true); err != nil { + return err + } + return nil +} + +func (o *V2LogsCursorResponse) GetResource() *string { + return types.String("logs") } func (o *V2LogsCursorResponse) GetCursor() V2LogsCursorResponseCursor { diff --git a/pkg/client/models/components/v2queryparams.go b/pkg/client/models/components/v2queryparams.go new file mode 100644 index 000000000..c143b2e42 --- /dev/null +++ b/pkg/client/models/components/v2queryparams.go @@ -0,0 +1,410 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +import ( + "errors" + "fmt" + "github.com/formancehq/ledger/pkg/client/internal/utils" + "github.com/formancehq/ledger/pkg/client/types" + "time" +) + +type QueryTemplateVolumeParams struct { + resource *string `const:"volumes" json:"resource,omitempty"` + UseInsertionDate any `json:"useInsertionDate,omitempty"` + GroupLvl any `json:"groupLvl,omitempty"` + // The maximum number of results to return per page. + // + PageSize *int64 `json:"pageSize,omitempty"` + // Parameter used in pagination requests. Maximum page size is set to 15. + // Set to the value of next for the next page of results. + // Set to the value of previous for the previous page of results. + // No other parameters can be set when this parameter is set. + // + Cursor *string `json:"cursor,omitempty"` + Expand *string `json:"expand,omitempty"` + Pit *time.Time `json:"pit,omitempty"` + // Sort results using a field name and order (ascending or descending). + // Format: `:`, where `` is the field name and `` is either `asc` or `desc`. + // + Sort any `json:"sort,omitempty"` +} + +func (q QueryTemplateVolumeParams) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(q, "", false) +} + +func (q *QueryTemplateVolumeParams) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &q, "", false, true); err != nil { + return err + } + return nil +} + +func (o *QueryTemplateVolumeParams) GetResource() *string { + return types.String("volumes") +} + +func (o *QueryTemplateVolumeParams) GetUseInsertionDate() any { + if o == nil { + return nil + } + return o.UseInsertionDate +} + +func (o *QueryTemplateVolumeParams) GetGroupLvl() any { + if o == nil { + return nil + } + return o.GroupLvl +} + +func (o *QueryTemplateVolumeParams) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *QueryTemplateVolumeParams) GetCursor() *string { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *QueryTemplateVolumeParams) GetExpand() *string { + if o == nil { + return nil + } + return o.Expand +} + +func (o *QueryTemplateVolumeParams) GetPit() *time.Time { + if o == nil { + return nil + } + return o.Pit +} + +func (o *QueryTemplateVolumeParams) GetSort() any { + if o == nil { + return nil + } + return o.Sort +} + +type QueryTemplateLogParams struct { + resource *string `const:"logs" json:"resource,omitempty"` + // The maximum number of results to return per page. + // + PageSize *int64 `json:"pageSize,omitempty"` + // Parameter used in pagination requests. Maximum page size is set to 15. + // Set to the value of next for the next page of results. + // Set to the value of previous for the previous page of results. + // No other parameters can be set when this parameter is set. + // + Cursor *string `json:"cursor,omitempty"` + Expand *string `json:"expand,omitempty"` + Pit *time.Time `json:"pit,omitempty"` + // Sort results using a field name and order (ascending or descending). + // Format: `:`, where `` is the field name and `` is either `asc` or `desc`. + // + Sort any `json:"sort,omitempty"` +} + +func (q QueryTemplateLogParams) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(q, "", false) +} + +func (q *QueryTemplateLogParams) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &q, "", false, true); err != nil { + return err + } + return nil +} + +func (o *QueryTemplateLogParams) GetResource() *string { + return types.String("logs") +} + +func (o *QueryTemplateLogParams) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *QueryTemplateLogParams) GetCursor() *string { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *QueryTemplateLogParams) GetExpand() *string { + if o == nil { + return nil + } + return o.Expand +} + +func (o *QueryTemplateLogParams) GetPit() *time.Time { + if o == nil { + return nil + } + return o.Pit +} + +func (o *QueryTemplateLogParams) GetSort() any { + if o == nil { + return nil + } + return o.Sort +} + +type QueryTemplateTransactionParams struct { + resource *string `const:"transactions" json:"resource,omitempty"` + // The maximum number of results to return per page. + // + PageSize *int64 `json:"pageSize,omitempty"` + // Parameter used in pagination requests. Maximum page size is set to 15. + // Set to the value of next for the next page of results. + // Set to the value of previous for the previous page of results. + // No other parameters can be set when this parameter is set. + // + Cursor *string `json:"cursor,omitempty"` + Expand *string `json:"expand,omitempty"` + Pit *time.Time `json:"pit,omitempty"` + // Sort results using a field name and order (ascending or descending). + // Format: `:`, where `` is the field name and `` is either `asc` or `desc`. + // + Sort any `json:"sort,omitempty"` +} + +func (q QueryTemplateTransactionParams) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(q, "", false) +} + +func (q *QueryTemplateTransactionParams) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &q, "", false, true); err != nil { + return err + } + return nil +} + +func (o *QueryTemplateTransactionParams) GetResource() *string { + return types.String("transactions") +} + +func (o *QueryTemplateTransactionParams) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *QueryTemplateTransactionParams) GetCursor() *string { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *QueryTemplateTransactionParams) GetExpand() *string { + if o == nil { + return nil + } + return o.Expand +} + +func (o *QueryTemplateTransactionParams) GetPit() *time.Time { + if o == nil { + return nil + } + return o.Pit +} + +func (o *QueryTemplateTransactionParams) GetSort() any { + if o == nil { + return nil + } + return o.Sort +} + +type QueryTemplateAccountParams struct { + resource *string `const:"accounts" json:"resource,omitempty"` + // The maximum number of results to return per page. + // + PageSize *int64 `json:"pageSize,omitempty"` + // Parameter used in pagination requests. Maximum page size is set to 15. + // Set to the value of next for the next page of results. + // Set to the value of previous for the previous page of results. + // No other parameters can be set when this parameter is set. + // + Cursor *string `json:"cursor,omitempty"` + Expand *string `json:"expand,omitempty"` + Pit *time.Time `json:"pit,omitempty"` + // Sort results using a field name and order (ascending or descending). + // Format: `:`, where `` is the field name and `` is either `asc` or `desc`. + // + Sort any `json:"sort,omitempty"` +} + +func (q QueryTemplateAccountParams) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(q, "", false) +} + +func (q *QueryTemplateAccountParams) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &q, "", false, true); err != nil { + return err + } + return nil +} + +func (o *QueryTemplateAccountParams) GetResource() *string { + return types.String("accounts") +} + +func (o *QueryTemplateAccountParams) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *QueryTemplateAccountParams) GetCursor() *string { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *QueryTemplateAccountParams) GetExpand() *string { + if o == nil { + return nil + } + return o.Expand +} + +func (o *QueryTemplateAccountParams) GetPit() *time.Time { + if o == nil { + return nil + } + return o.Pit +} + +func (o *QueryTemplateAccountParams) GetSort() any { + if o == nil { + return nil + } + return o.Sort +} + +type V2QueryParamsType string + +const ( + V2QueryParamsTypeQueryTemplateAccountParams V2QueryParamsType = "QueryTemplateAccountParams" + V2QueryParamsTypeQueryTemplateTransactionParams V2QueryParamsType = "QueryTemplateTransactionParams" + V2QueryParamsTypeQueryTemplateLogParams V2QueryParamsType = "QueryTemplateLogParams" + V2QueryParamsTypeQueryTemplateVolumeParams V2QueryParamsType = "QueryTemplateVolumeParams" +) + +type V2QueryParams struct { + QueryTemplateAccountParams *QueryTemplateAccountParams `queryParam:"inline"` + QueryTemplateTransactionParams *QueryTemplateTransactionParams `queryParam:"inline"` + QueryTemplateLogParams *QueryTemplateLogParams `queryParam:"inline"` + QueryTemplateVolumeParams *QueryTemplateVolumeParams `queryParam:"inline"` + + Type V2QueryParamsType +} + +func CreateV2QueryParamsQueryTemplateAccountParams(queryTemplateAccountParams QueryTemplateAccountParams) V2QueryParams { + typ := V2QueryParamsTypeQueryTemplateAccountParams + + return V2QueryParams{ + QueryTemplateAccountParams: &queryTemplateAccountParams, + Type: typ, + } +} + +func CreateV2QueryParamsQueryTemplateTransactionParams(queryTemplateTransactionParams QueryTemplateTransactionParams) V2QueryParams { + typ := V2QueryParamsTypeQueryTemplateTransactionParams + + return V2QueryParams{ + QueryTemplateTransactionParams: &queryTemplateTransactionParams, + Type: typ, + } +} + +func CreateV2QueryParamsQueryTemplateLogParams(queryTemplateLogParams QueryTemplateLogParams) V2QueryParams { + typ := V2QueryParamsTypeQueryTemplateLogParams + + return V2QueryParams{ + QueryTemplateLogParams: &queryTemplateLogParams, + Type: typ, + } +} + +func CreateV2QueryParamsQueryTemplateVolumeParams(queryTemplateVolumeParams QueryTemplateVolumeParams) V2QueryParams { + typ := V2QueryParamsTypeQueryTemplateVolumeParams + + return V2QueryParams{ + QueryTemplateVolumeParams: &queryTemplateVolumeParams, + Type: typ, + } +} + +func (u *V2QueryParams) UnmarshalJSON(data []byte) error { + + var queryTemplateAccountParams QueryTemplateAccountParams = QueryTemplateAccountParams{} + if err := utils.UnmarshalJSON(data, &queryTemplateAccountParams, "", true, true); err == nil { + u.QueryTemplateAccountParams = &queryTemplateAccountParams + u.Type = V2QueryParamsTypeQueryTemplateAccountParams + return nil + } + + var queryTemplateTransactionParams QueryTemplateTransactionParams = QueryTemplateTransactionParams{} + if err := utils.UnmarshalJSON(data, &queryTemplateTransactionParams, "", true, true); err == nil { + u.QueryTemplateTransactionParams = &queryTemplateTransactionParams + u.Type = V2QueryParamsTypeQueryTemplateTransactionParams + return nil + } + + var queryTemplateLogParams QueryTemplateLogParams = QueryTemplateLogParams{} + if err := utils.UnmarshalJSON(data, &queryTemplateLogParams, "", true, true); err == nil { + u.QueryTemplateLogParams = &queryTemplateLogParams + u.Type = V2QueryParamsTypeQueryTemplateLogParams + return nil + } + + var queryTemplateVolumeParams QueryTemplateVolumeParams = QueryTemplateVolumeParams{} + if err := utils.UnmarshalJSON(data, &queryTemplateVolumeParams, "", true, true); err == nil { + u.QueryTemplateVolumeParams = &queryTemplateVolumeParams + u.Type = V2QueryParamsTypeQueryTemplateVolumeParams + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for V2QueryParams", string(data)) +} + +func (u V2QueryParams) MarshalJSON() ([]byte, error) { + if u.QueryTemplateAccountParams != nil { + return utils.MarshalJSON(u.QueryTemplateAccountParams, "", true) + } + + if u.QueryTemplateTransactionParams != nil { + return utils.MarshalJSON(u.QueryTemplateTransactionParams, "", true) + } + + if u.QueryTemplateLogParams != nil { + return utils.MarshalJSON(u.QueryTemplateLogParams, "", true) + } + + if u.QueryTemplateVolumeParams != nil { + return utils.MarshalJSON(u.QueryTemplateVolumeParams, "", true) + } + + return nil, errors.New("could not marshal union type V2QueryParams: all fields are null") +} diff --git a/pkg/client/models/components/v2queryresource.go b/pkg/client/models/components/v2queryresource.go new file mode 100644 index 000000000..47ad16b56 --- /dev/null +++ b/pkg/client/models/components/v2queryresource.go @@ -0,0 +1,40 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +import ( + "encoding/json" + "fmt" +) + +type V2QueryResource string + +const ( + V2QueryResourceTransactions V2QueryResource = "transactions" + V2QueryResourceAccounts V2QueryResource = "accounts" + V2QueryResourceLogs V2QueryResource = "logs" + V2QueryResourceVolumes V2QueryResource = "volumes" +) + +func (e V2QueryResource) ToPointer() *V2QueryResource { + return &e +} +func (e *V2QueryResource) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "transactions": + fallthrough + case "accounts": + fallthrough + case "logs": + fallthrough + case "volumes": + *e = V2QueryResource(v) + return nil + default: + return fmt.Errorf("invalid value for V2QueryResource: %v", v) + } +} diff --git a/pkg/client/models/components/v2querytemplate.go b/pkg/client/models/components/v2querytemplate.go new file mode 100644 index 000000000..ae22650f9 --- /dev/null +++ b/pkg/client/models/components/v2querytemplate.go @@ -0,0 +1,46 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V2QueryTemplate struct { + Name any `json:"name,omitempty"` + Resource *V2QueryResource `json:"resource,omitempty"` + Params *V2QueryParams `json:"params,omitempty"` + Vars map[string]V2QueryTemplateVar `json:"vars,omitempty"` + Body map[string]any `json:"body,omitempty"` +} + +func (o *V2QueryTemplate) GetName() any { + if o == nil { + return nil + } + return o.Name +} + +func (o *V2QueryTemplate) GetResource() *V2QueryResource { + if o == nil { + return nil + } + return o.Resource +} + +func (o *V2QueryTemplate) GetParams() *V2QueryParams { + if o == nil { + return nil + } + return o.Params +} + +func (o *V2QueryTemplate) GetVars() map[string]V2QueryTemplateVar { + if o == nil { + return nil + } + return o.Vars +} + +func (o *V2QueryTemplate) GetBody() map[string]any { + if o == nil { + return nil + } + return o.Body +} diff --git a/pkg/client/models/components/v2querytemplatevar.go b/pkg/client/models/components/v2querytemplatevar.go new file mode 100644 index 000000000..21533a768 --- /dev/null +++ b/pkg/client/models/components/v2querytemplatevar.go @@ -0,0 +1,22 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V2QueryTemplateVar struct { + Type any `json:"type,omitempty"` + Default any `json:"default,omitempty"` +} + +func (o *V2QueryTemplateVar) GetType() any { + if o == nil { + return nil + } + return o.Type +} + +func (o *V2QueryTemplateVar) GetDefault() any { + if o == nil { + return nil + } + return o.Default +} diff --git a/pkg/client/models/components/v2schema.go b/pkg/client/models/components/v2schema.go index 062d5ab54..1b1261297 100644 --- a/pkg/client/models/components/v2schema.go +++ b/pkg/client/models/components/v2schema.go @@ -17,6 +17,8 @@ type V2Schema struct { Chart map[string]V2ChartSegment `json:"chart"` // Transaction templates Transactions map[string]V2TransactionTemplate `json:"transactions"` + // Query templates + Queries map[string]V2QueryTemplate `json:"queries,omitempty"` } func (v V2Schema) MarshalJSON() ([]byte, error) { @@ -57,3 +59,10 @@ func (o *V2Schema) GetTransactions() map[string]V2TransactionTemplate { } return o.Transactions } + +func (o *V2Schema) GetQueries() map[string]V2QueryTemplate { + if o == nil { + return nil + } + return o.Queries +} diff --git a/pkg/client/models/components/v2schemadata.go b/pkg/client/models/components/v2schemadata.go index f5ef95b55..541b9cd57 100644 --- a/pkg/client/models/components/v2schemadata.go +++ b/pkg/client/models/components/v2schemadata.go @@ -8,6 +8,8 @@ type V2SchemaData struct { Chart map[string]V2ChartSegment `json:"chart"` // Transaction templates Transactions map[string]V2TransactionTemplate `json:"transactions"` + // Query templates + Queries map[string]V2QueryTemplate `json:"queries,omitempty"` } func (o *V2SchemaData) GetChart() map[string]V2ChartSegment { @@ -23,3 +25,10 @@ func (o *V2SchemaData) GetTransactions() map[string]V2TransactionTemplate { } return o.Transactions } + +func (o *V2SchemaData) GetQueries() map[string]V2QueryTemplate { + if o == nil { + return nil + } + return o.Queries +} diff --git a/pkg/client/models/components/v2transactionscursorresponse.go b/pkg/client/models/components/v2transactionscursorresponse.go index bb46022b8..58929c127 100644 --- a/pkg/client/models/components/v2transactionscursorresponse.go +++ b/pkg/client/models/components/v2transactionscursorresponse.go @@ -2,6 +2,11 @@ package components +import ( + "github.com/formancehq/ledger/pkg/client/internal/utils" + "github.com/formancehq/ledger/pkg/client/types" +) + type V2TransactionsCursorResponseCursor struct { PageSize int64 `json:"pageSize"` HasMore bool `json:"hasMore"` @@ -46,7 +51,23 @@ func (o *V2TransactionsCursorResponseCursor) GetData() []V2Transaction { } type V2TransactionsCursorResponse struct { - Cursor V2TransactionsCursorResponseCursor `json:"cursor"` + resource *string `const:"transactions" json:"resource,omitempty"` + Cursor V2TransactionsCursorResponseCursor `json:"cursor"` +} + +func (v V2TransactionsCursorResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V2TransactionsCursorResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, true); err != nil { + return err + } + return nil +} + +func (o *V2TransactionsCursorResponse) GetResource() *string { + return types.String("transactions") } func (o *V2TransactionsCursorResponse) GetCursor() V2TransactionsCursorResponseCursor { diff --git a/pkg/client/models/components/v2volumeswithbalancecursorresponse.go b/pkg/client/models/components/v2volumeswithbalancecursorresponse.go index b03a5cf09..ad94ff902 100644 --- a/pkg/client/models/components/v2volumeswithbalancecursorresponse.go +++ b/pkg/client/models/components/v2volumeswithbalancecursorresponse.go @@ -2,6 +2,11 @@ package components +import ( + "github.com/formancehq/ledger/pkg/client/internal/utils" + "github.com/formancehq/ledger/pkg/client/types" +) + type V2VolumesWithBalanceCursorResponseCursor struct { PageSize int64 `json:"pageSize"` HasMore bool `json:"hasMore"` @@ -46,7 +51,23 @@ func (o *V2VolumesWithBalanceCursorResponseCursor) GetData() []V2VolumesWithBala } type V2VolumesWithBalanceCursorResponse struct { - Cursor V2VolumesWithBalanceCursorResponseCursor `json:"cursor"` + resource *string `const:"volumes" json:"resource,omitempty"` + Cursor V2VolumesWithBalanceCursorResponseCursor `json:"cursor"` +} + +func (v V2VolumesWithBalanceCursorResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V2VolumesWithBalanceCursorResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, true); err != nil { + return err + } + return nil +} + +func (o *V2VolumesWithBalanceCursorResponse) GetResource() *string { + return types.String("volumes") } func (o *V2VolumesWithBalanceCursorResponse) GetCursor() V2VolumesWithBalanceCursorResponseCursor { diff --git a/pkg/client/models/operations/v2runquery.go b/pkg/client/models/operations/v2runquery.go new file mode 100644 index 000000000..45bf17944 --- /dev/null +++ b/pkg/client/models/operations/v2runquery.go @@ -0,0 +1,309 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/formancehq/ledger/pkg/client/internal/utils" + "github.com/formancehq/ledger/pkg/client/models/components" + "time" +) + +// V2RunQueryQueryParamOrder - Deprecated: Use sort param +type V2RunQueryQueryParamOrder string + +const ( + V2RunQueryQueryParamOrderEffective V2RunQueryQueryParamOrder = "effective" +) + +func (e V2RunQueryQueryParamOrder) ToPointer() *V2RunQueryQueryParamOrder { + return &e +} +func (e *V2RunQueryQueryParamOrder) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "effective": + *e = V2RunQueryQueryParamOrder(v) + return nil + default: + return fmt.Errorf("invalid value for V2RunQueryQueryParamOrder: %v", v) + } +} + +type V2RunQueryRequestBody struct { + Cursor any `json:"cursor,omitempty"` + Params any `json:"params,omitempty"` + Vars map[string]string `json:"vars,omitempty"` +} + +func (o *V2RunQueryRequestBody) GetCursor() any { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *V2RunQueryRequestBody) GetParams() any { + if o == nil { + return nil + } + return o.Params +} + +func (o *V2RunQueryRequestBody) GetVars() map[string]string { + if o == nil { + return nil + } + return o.Vars +} + +type V2RunQueryRequest struct { + // Name of the ledger. + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + // Schema version to use for validation + SchemaVersion string `queryParam:"style=form,explode=true,name=schemaVersion"` + // Query template ID. + ID string `pathParam:"style=simple,explode=false,name=id"` + // The maximum number of results to return per page. + // + PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` + // Parameter used in pagination requests. Maximum page size is set to 15. + // Set to the value of next for the next page of results. + // Set to the value of previous for the previous page of results. + // No other parameters can be set when this parameter is set. + // + Cursor *string `queryParam:"style=form,explode=true,name=cursor"` + Expand *string `queryParam:"style=form,explode=true,name=expand"` + Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` + // Deprecated: Use sort param + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + Order *V2RunQueryQueryParamOrder `queryParam:"style=form,explode=true,name=order"` + Reverse *bool `queryParam:"style=form,explode=true,name=reverse"` + // Sort results using a field name and order (ascending or descending). + // Format: `:`, where `` is the field name and `` is either `asc` or `desc`. + // + Sort *string `queryParam:"style=form,explode=true,name=sort"` + RequestBody V2RunQueryRequestBody `request:"mediaType=application/json"` +} + +func (v V2RunQueryRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V2RunQueryRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, false); err != nil { + return err + } + return nil +} + +func (o *V2RunQueryRequest) GetLedger() string { + if o == nil { + return "" + } + return o.Ledger +} + +func (o *V2RunQueryRequest) GetSchemaVersion() string { + if o == nil { + return "" + } + return o.SchemaVersion +} + +func (o *V2RunQueryRequest) GetID() string { + if o == nil { + return "" + } + return o.ID +} + +func (o *V2RunQueryRequest) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *V2RunQueryRequest) GetCursor() *string { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *V2RunQueryRequest) GetExpand() *string { + if o == nil { + return nil + } + return o.Expand +} + +func (o *V2RunQueryRequest) GetPit() *time.Time { + if o == nil { + return nil + } + return o.Pit +} + +func (o *V2RunQueryRequest) GetOrder() *V2RunQueryQueryParamOrder { + if o == nil { + return nil + } + return o.Order +} + +func (o *V2RunQueryRequest) GetReverse() *bool { + if o == nil { + return nil + } + return o.Reverse +} + +func (o *V2RunQueryRequest) GetSort() *string { + if o == nil { + return nil + } + return o.Sort +} + +func (o *V2RunQueryRequest) GetRequestBody() V2RunQueryRequestBody { + if o == nil { + return V2RunQueryRequestBody{} + } + return o.RequestBody +} + +type V2RunQueryResponseBodyType string + +const ( + V2RunQueryResponseBodyTypeV2TransactionsCursorResponse V2RunQueryResponseBodyType = "V2TransactionsCursorResponse" + V2RunQueryResponseBodyTypeV2AccountsCursorResponse V2RunQueryResponseBodyType = "V2AccountsCursorResponse" + V2RunQueryResponseBodyTypeV2LogsCursorResponse V2RunQueryResponseBodyType = "V2LogsCursorResponse" + V2RunQueryResponseBodyTypeV2VolumesWithBalanceCursorResponse V2RunQueryResponseBodyType = "V2VolumesWithBalanceCursorResponse" +) + +// V2RunQueryResponseBody - OK +type V2RunQueryResponseBody struct { + V2TransactionsCursorResponse *components.V2TransactionsCursorResponse `queryParam:"inline"` + V2AccountsCursorResponse *components.V2AccountsCursorResponse `queryParam:"inline"` + V2LogsCursorResponse *components.V2LogsCursorResponse `queryParam:"inline"` + V2VolumesWithBalanceCursorResponse *components.V2VolumesWithBalanceCursorResponse `queryParam:"inline"` + + Type V2RunQueryResponseBodyType +} + +func CreateV2RunQueryResponseBodyV2TransactionsCursorResponse(v2TransactionsCursorResponse components.V2TransactionsCursorResponse) V2RunQueryResponseBody { + typ := V2RunQueryResponseBodyTypeV2TransactionsCursorResponse + + return V2RunQueryResponseBody{ + V2TransactionsCursorResponse: &v2TransactionsCursorResponse, + Type: typ, + } +} + +func CreateV2RunQueryResponseBodyV2AccountsCursorResponse(v2AccountsCursorResponse components.V2AccountsCursorResponse) V2RunQueryResponseBody { + typ := V2RunQueryResponseBodyTypeV2AccountsCursorResponse + + return V2RunQueryResponseBody{ + V2AccountsCursorResponse: &v2AccountsCursorResponse, + Type: typ, + } +} + +func CreateV2RunQueryResponseBodyV2LogsCursorResponse(v2LogsCursorResponse components.V2LogsCursorResponse) V2RunQueryResponseBody { + typ := V2RunQueryResponseBodyTypeV2LogsCursorResponse + + return V2RunQueryResponseBody{ + V2LogsCursorResponse: &v2LogsCursorResponse, + Type: typ, + } +} + +func CreateV2RunQueryResponseBodyV2VolumesWithBalanceCursorResponse(v2VolumesWithBalanceCursorResponse components.V2VolumesWithBalanceCursorResponse) V2RunQueryResponseBody { + typ := V2RunQueryResponseBodyTypeV2VolumesWithBalanceCursorResponse + + return V2RunQueryResponseBody{ + V2VolumesWithBalanceCursorResponse: &v2VolumesWithBalanceCursorResponse, + Type: typ, + } +} + +func (u *V2RunQueryResponseBody) UnmarshalJSON(data []byte) error { + + var v2TransactionsCursorResponse components.V2TransactionsCursorResponse = components.V2TransactionsCursorResponse{} + if err := utils.UnmarshalJSON(data, &v2TransactionsCursorResponse, "", true, true); err == nil { + u.V2TransactionsCursorResponse = &v2TransactionsCursorResponse + u.Type = V2RunQueryResponseBodyTypeV2TransactionsCursorResponse + return nil + } + + var v2AccountsCursorResponse components.V2AccountsCursorResponse = components.V2AccountsCursorResponse{} + if err := utils.UnmarshalJSON(data, &v2AccountsCursorResponse, "", true, true); err == nil { + u.V2AccountsCursorResponse = &v2AccountsCursorResponse + u.Type = V2RunQueryResponseBodyTypeV2AccountsCursorResponse + return nil + } + + var v2LogsCursorResponse components.V2LogsCursorResponse = components.V2LogsCursorResponse{} + if err := utils.UnmarshalJSON(data, &v2LogsCursorResponse, "", true, true); err == nil { + u.V2LogsCursorResponse = &v2LogsCursorResponse + u.Type = V2RunQueryResponseBodyTypeV2LogsCursorResponse + return nil + } + + var v2VolumesWithBalanceCursorResponse components.V2VolumesWithBalanceCursorResponse = components.V2VolumesWithBalanceCursorResponse{} + if err := utils.UnmarshalJSON(data, &v2VolumesWithBalanceCursorResponse, "", true, true); err == nil { + u.V2VolumesWithBalanceCursorResponse = &v2VolumesWithBalanceCursorResponse + u.Type = V2RunQueryResponseBodyTypeV2VolumesWithBalanceCursorResponse + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for V2RunQueryResponseBody", string(data)) +} + +func (u V2RunQueryResponseBody) MarshalJSON() ([]byte, error) { + if u.V2TransactionsCursorResponse != nil { + return utils.MarshalJSON(u.V2TransactionsCursorResponse, "", true) + } + + if u.V2AccountsCursorResponse != nil { + return utils.MarshalJSON(u.V2AccountsCursorResponse, "", true) + } + + if u.V2LogsCursorResponse != nil { + return utils.MarshalJSON(u.V2LogsCursorResponse, "", true) + } + + if u.V2VolumesWithBalanceCursorResponse != nil { + return utils.MarshalJSON(u.V2VolumesWithBalanceCursorResponse, "", true) + } + + return nil, errors.New("could not marshal union type V2RunQueryResponseBody: all fields are null") +} + +type V2RunQueryResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // OK + OneOf *V2RunQueryResponseBody +} + +func (o *V2RunQueryResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V2RunQueryResponse) GetOneOf() *V2RunQueryResponseBody { + if o == nil { + return nil + } + return o.OneOf +} diff --git a/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log b/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log index c8cc22ddf..ded8e6a26 100644 --- a/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log +++ b/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log @@ -102,13 +102,21 @@ V2CreateLedgerRequest (ledger: string, V2CreateLedgerRequest: V2CreateLedgerRequ V2CreateLedgerRequest (bucket: string, metadata: map, features: map) V2CreateLedgerResponse (HttpMeta: HTTPMetadata) V2InsertSchemaRequest (ledger: string, version: string, Idempotency-Key: string ...) - V2SchemaData (chart: map, transactions: map) + V2SchemaData (chart: map, transactions: map, queries: map) V2ChartSegment (.self: class, .pattern: string, .rules: V2ChartAccountRules ...) Self (empty) V2ChartAccountRules (empty) V2ChartAccountMetadata (default: string) V2TransactionTemplate (description: string, script: string, runtime: Runtime) Runtime (enum: experimental-interpreter, machine) + V2QueryTemplate (name: any, resource: V2QueryResource, params: V2QueryParams ...) + V2QueryResource (enum: transactions, accounts, logs ...) + V2QueryParams (union) + QueryTemplateAccountParams (resource: string, pageSize: integer, cursor: string ...) + QueryTemplateTransactionParams (resource: string, pageSize: integer, cursor: string ...) + QueryTemplateLogParams (resource: string, pageSize: integer, cursor: string ...) + QueryTemplateVolumeParams (resource: string, useInsertionDate: any, groupLvl: any ...) + V2QueryTemplateVar (type: any, default: any) V2InsertSchemaResponse (HttpMeta: HTTPMetadata, Headers: map) V2GetSchemaRequest (ledger: string, version: string) V2GetSchemaResponse (HttpMeta: HTTPMetadata, V2SchemaResponse: V2SchemaResponse) @@ -160,7 +168,7 @@ V2CountAccountsRequest (ledger: string, pit: date-time, RequestBody: map) V2CountAccountsResponse (HttpMeta: HTTPMetadata, Headers: map) V2ListAccountsRequest (ledger: string, pageSize: integer, cursor: string ...) V2ListAccountsResponse (HttpMeta: HTTPMetadata, V2AccountsCursorResponse: V2AccountsCursorResponse) - V2AccountsCursorResponse (cursor: class) + V2AccountsCursorResponse (resource: string, cursor: class) V2AccountsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2Account (address: string, metadata: map, insertionDate: date-time ...) V2GetAccountRequest (ledger: string, address: string, expand: string ...) @@ -179,7 +187,7 @@ V2CountTransactionsResponse (HttpMeta: HTTPMetadata, Headers: map) V2ListTransactionsRequest (ledger: string, pageSize: integer, cursor: string ...) QueryParamOrder (enum: effective) V2ListTransactionsResponse (HttpMeta: HTTPMetadata, V2TransactionsCursorResponse: V2TransactionsCursorResponse) - V2TransactionsCursorResponse (cursor: class) + V2TransactionsCursorResponse (resource: string, cursor: class) V2TransactionsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2CreateTransactionRequest (ledger: string, dryRun: boolean, Idempotency-Key: string ...) V2CreateTransactionResponse (HttpMeta: HTTPMetadata, V2CreateTransactionResponse: V2CreateTransactionResponse, Headers: map) @@ -199,12 +207,12 @@ V2GetBalancesAggregatedResponse (HttpMeta: HTTPMetadata, V2AggregateBalancesResp V2AggregateBalancesResponse (data: map) V2GetVolumesWithBalancesRequest (pageSize: integer, cursor: string, ledger: string ...) V2GetVolumesWithBalancesResponse (HttpMeta: HTTPMetadata, V2VolumesWithBalanceCursorResponse: V2VolumesWithBalanceCursorResponse) - V2VolumesWithBalanceCursorResponse (cursor: class) + V2VolumesWithBalanceCursorResponse (resource: string, cursor: class) V2VolumesWithBalanceCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2VolumesWithBalance (account: string, asset: string, input: bigint ...) V2ListLogsRequest (ledger: string, pageSize: integer, cursor: string ...) V2ListLogsResponse (HttpMeta: HTTPMetadata, V2LogsCursorResponse: V2LogsCursorResponse) - V2LogsCursorResponse (cursor: class) + V2LogsCursorResponse (resource: string, cursor: class) V2LogsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) V2Log (id: bigint, type: enum, data: union ...) V2LogType (enum: NEW_TRANSACTION, SET_METADATA, REVERTED_TRANSACTION ...) @@ -223,6 +231,11 @@ V2ImportLogsRequest (ledger: string, V2ImportLogsRequest: V2ImportLogsRequest) V2ImportLogsResponse (HttpMeta: HTTPMetadata) V2ExportLogsRequest (ledger: string) V2ExportLogsResponse (HttpMeta: HTTPMetadata) +V2RunQueryRequest (ledger: string, schemaVersion: string, id: string ...) + V2RunQueryQueryParamOrder (enum: effective) + V2RunQueryRequestBody (cursor: any, params: any, vars: map) +V2RunQueryResponse (HttpMeta: HTTPMetadata, oneOf: union) + V2RunQueryResponseBody (union) V2ListExportersResponse (HttpMeta: HTTPMetadata, V2ListExportersResponse: V2ListExportersResponse) V2ListExportersResponse (cursor: class) V2ListExportersResponseCursor (cursor: class, data: array) diff --git a/pkg/client/v2.go b/pkg/client/v2.go index b2cec9d69..393590150 100644 --- a/pkg/client/v2.go +++ b/pkg/client/v2.go @@ -5862,6 +5862,226 @@ func (s *V2) ExportLogs(ctx context.Context, request operations.V2ExportLogsRequ } +// RunQuery - Run a query template +// Run a query template on a ledger +func (s *V2) RunQuery(ctx context.Context, request operations.V2RunQueryRequest, opts ...operations.Option) (*operations.V2RunQueryResponse, error) { + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/queries/{id}/run", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "v2RunQuery", + OAuth2Scopes: []string{"ledger:read", "ledger:read"}, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "RequestBody", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "429", + "500", + "502", + "503", + "504", + }, + }, func() (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"default"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.V2RunQueryResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out operations.V2RunQueryResponseBody + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.OneOf = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.V2ErrorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + // ListExporters - List exporters func (s *V2) ListExporters(ctx context.Context, opts ...operations.Option) (*operations.V2ListExportersResponse, error) { o := operations.Options{} diff --git a/test/e2e/api_queries_run_test.go b/test/e2e/api_queries_run_test.go new file mode 100644 index 000000000..6749583e6 --- /dev/null +++ b/test/e2e/api_queries_run_test.go @@ -0,0 +1,185 @@ +//go:build it + +package test_suite + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/formancehq/go-libs/v3/logging" + "github.com/formancehq/go-libs/v3/pointer" + . "github.com/formancehq/go-libs/v3/testing/deferred/ginkgo" + "github.com/formancehq/go-libs/v3/testing/platform/pgtesting" + "github.com/formancehq/go-libs/v3/testing/testservice" + + "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client/models/operations" + . "github.com/formancehq/ledger/pkg/testserver" + "github.com/formancehq/ledger/pkg/testserver/ginkgo" +) + +var _ = Context("Ledger query API tests", func() { + var ( + db = UseTemplatedDatabase() + ctx = logging.TestingContext() + ) + + testServer := ginkgo.DeferTestServer( + DeferMap(db, (*pgtesting.Database).ConnectionOptions), + testservice.WithInstruments( + testservice.OutputInstrumentation(GinkgoWriter), + ), + testservice.WithLogger(GinkgoT()), + ) + + When("creating a ledger", func() { + BeforeEach(func(specContext SpecContext) { + _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.CreateLedger(ctx, operations.V2CreateLedgerRequest{ + Ledger: "default", + }) + Expect(err).To(BeNil()) + }) + + schemaVersion := "v1.0.0" + When("inserting schema and transactions", func() { + BeforeEach(func(specContext SpecContext) { + // Schema v1.0.0 - Basic validation + _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.InsertSchema(ctx, operations.V2InsertSchemaRequest{ + Ledger: "default", + Version: schemaVersion, + V2SchemaData: components.V2SchemaData{ + Chart: map[string]components.V2ChartSegment{ + "world": { + DotSelf: &components.DotSelf{}, + }, + "bank": { + AdditionalProperties: map[string]components.V2ChartSegment{ + "$bankID": { + DotPattern: pointer.For("^[0-9]{3}$"), + }, + }, + }, + "foo": { + AdditionalProperties: map[string]components.V2ChartSegment{ + "$fooID": { + DotSelf: &components.DotSelf{}, + }, + }, + }, + "bar": { + AdditionalProperties: map[string]components.V2ChartSegment{ + "$barID": { + DotSelf: &components.DotSelf{}, + }, + }, + }, + }, + Transactions: map[string]components.V2TransactionTemplate{ + "DEPOSIT": { + Script: ` + vars { + account $dest + monetary $mon + } + send $mon ( + source = @world + destination = $dest + )`, + }, + }, + Queries: map[string]components.V2QueryTemplate{ + "CUSTOMERS": { + Name: "Balance of customers with matching category and hat", + Resource: components.V2QueryResourceAccounts.ToPointer(), + Vars: map[string]components.V2QueryTemplateVar{ + "category": { + Type: "string", + Default: "foo", + }, + "hat_type": { + Type: "string", + }, + }, + Body: map[string]any{ + "$and": []any{ + map[string]any{ + "$match": map[string]any{ + "address": "$category:", + }, + }, + map[string]any{ + "$match": map[string]any{ + "metadata[hat_type]": "$hat_type", + }, + }, + }, + }, + }, + }, + }, + }) + Expect(err).To(BeNil()) + + _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.AddMetadataToAccount(ctx, operations.V2AddMetadataToAccountRequest{ + Ledger: "default", + SchemaVersion: &schemaVersion, + Address: "foo:012", + RequestBody: map[string]string{ + "hat_type": "cap", + }, + }) + Expect(err).To(BeNil()) + + for _, tx := range []components.V2PostTransactionScript{ + { + Template: pointer.For("DEPOSIT"), + Vars: map[string]string{ + "dest": "foo:000", + "mon": "COIN 42", + }, + }, + { + Template: pointer.For("DEPOSIT"), + Vars: map[string]string{ + "dest": "foo:001", + "mon": "COIN 7", + }, + }, + { + Template: pointer.For("DEPOSIT"), + Vars: map[string]string{ + "dest": "bar:000", + "mon": "COIN 52", + }, + }, + } { + _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.CreateTransaction(ctx, operations.V2CreateTransactionRequest{ + Ledger: "default", + SchemaVersion: &schemaVersion, + V2PostTransaction: components.V2PostTransaction{ + Force: pointer.For(true), + Script: &tx, + }, + }) + Expect(err).To(BeNil()) + } + }) + + It("should return correct results", func(specContext SpecContext) { + res, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.RunQuery(ctx, operations.V2RunQueryRequest{ + Ledger: "default", + ID: "CUSTOMERS", + SchemaVersion: schemaVersion, + RequestBody: operations.V2RunQueryRequestBody{ + Vars: map[string]string{ + "category": "foo", + "hat_type": "cap", + }, + }, + }) + Expect(err).To(BeNil()) + Expect(len(res.OneOf.V2AccountsCursorResponse.Cursor.Data)).To(Equal(1)) + }) + }) + }) +}) diff --git a/tools/generator/go.mod b/tools/generator/go.mod index b80bb2a76..6d9593c95 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -8,8 +8,10 @@ replace github.com/formancehq/ledger => ../.. replace github.com/formancehq/ledger/pkg/client => ../../pkg/client +replace github.com/formancehq/go-libs/v3 v3.5.0 => ../../../go-libs + require ( - github.com/formancehq/go-libs/v3 v3.5.0 + github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.10.1 @@ -42,6 +44,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect diff --git a/tools/generator/go.sum b/tools/generator/go.sum index aa730f04d..841ab3b52 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -116,8 +116,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v3 v3.5.0 h1:8RLGsQ2EuAMSL89rxbjcIyvEM9tavJIlomPcvIqRNzA= -github.com/formancehq/go-libs/v3 v3.5.0/go.mod h1:Lr0qE3ioCTFlm+BuXSwB7qpGF12/IfKYOpFvszTFRJk= +github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07 h1:xATZNNolHaP0JRjZGxgNsGKdKLV5yUU9r80bKm9HQN4= +github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07/go.mod h1:3kzr8nMPSbUEaQzaSwesUgn8QFwsbx2NHQC0P1vXat4= github.com/formancehq/numscript v0.0.21 h1:aeudLAKGL8u4TYBiJkZFKw0ElC4V44iJCf/xOGMJKIA= github.com/formancehq/numscript v0.0.21/go.mod h1:hC/VY5Vg04F5QkgdPPc6z/YsS/vh8V1qVJVa1VWnYMA= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= diff --git a/tools/provisioner/go.mod b/tools/provisioner/go.mod index a4dc7fd33..1b340f1ab 100644 --- a/tools/provisioner/go.mod +++ b/tools/provisioner/go.mod @@ -9,8 +9,10 @@ replace ( github.com/formancehq/ledger/pkg/client => ../../pkg/client ) +replace github.com/formancehq/go-libs/v3 v3.5.0 => ../../../go-libs + require ( - github.com/formancehq/go-libs/v3 v3.5.0 + github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/google/go-cmp v0.7.0 diff --git a/tools/provisioner/go.sum b/tools/provisioner/go.sum index 4ab356d71..2c21a4606 100644 --- a/tools/provisioner/go.sum +++ b/tools/provisioner/go.sum @@ -114,8 +114,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v3 v3.5.0 h1:8RLGsQ2EuAMSL89rxbjcIyvEM9tavJIlomPcvIqRNzA= -github.com/formancehq/go-libs/v3 v3.5.0/go.mod h1:Lr0qE3ioCTFlm+BuXSwB7qpGF12/IfKYOpFvszTFRJk= +github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07 h1:xATZNNolHaP0JRjZGxgNsGKdKLV5yUU9r80bKm9HQN4= +github.com/formancehq/go-libs/v3 v3.6.1-0.20260203163702-856bac344d07/go.mod h1:3kzr8nMPSbUEaQzaSwesUgn8QFwsbx2NHQC0P1vXat4= github.com/formancehq/numscript v0.0.21 h1:aeudLAKGL8u4TYBiJkZFKw0ElC4V44iJCf/xOGMJKIA= github.com/formancehq/numscript v0.0.21/go.mod h1:hC/VY5Vg04F5QkgdPPc6z/YsS/vh8V1qVJVa1VWnYMA= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= From 2c951af16592386bf719315b1531ddb36fdca822 Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Thu, 5 Feb 2026 13:37:44 +0100 Subject: [PATCH 2/9] validate sort column --- internal/README.md | 14 +++++++------- internal/queries/filter_template.go | 10 ++++++++-- internal/queries/resources.go | 12 ++++++------ internal/query_template.go | 15 ++++++++++++--- internal/query_template_test.go | 20 ++++++++++++++++++++ 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/internal/README.md b/internal/README.md index e858b1ce1..442f8e042 100644 --- a/internal/README.md +++ b/internal/README.md @@ -1546,7 +1546,7 @@ func (p Postings) Validate() (int, error) -## type [QueryTemplate]() +## type [QueryTemplate]() @@ -1561,7 +1561,7 @@ type QueryTemplate struct { ``` -### func \(QueryTemplate\) [Validate]() +### func \(QueryTemplate\) [Validate]() ```go func (q QueryTemplate) Validate() error @@ -1570,7 +1570,7 @@ func (q QueryTemplate) Validate() error Validate a query template -## type [QueryTemplateParams]() +## type [QueryTemplateParams]() @@ -1587,7 +1587,7 @@ type QueryTemplateParams[Opts any] struct { ``` -### func \(QueryTemplateParams\[Opts\]\) [Overwrite]() +### func \(QueryTemplateParams\[Opts\]\) [Overwrite]() ```go func (q QueryTemplateParams[Opts]) Overwrite(others ...json.RawMessage) (*QueryTemplateParams[Opts], error) @@ -1596,7 +1596,7 @@ func (q QueryTemplateParams[Opts]) Overwrite(others ...json.RawMessage) (*QueryT -### func \(\*QueryTemplateParams\[Opts\]\) [UnmarshalJSON]() +### func \(\*QueryTemplateParams\[Opts\]\) [UnmarshalJSON]() ```go func (p *QueryTemplateParams[Opts]) UnmarshalJSON(b []byte) error @@ -1605,7 +1605,7 @@ func (p *QueryTemplateParams[Opts]) UnmarshalJSON(b []byte) error -## type [QueryTemplates]() +## type [QueryTemplates]() @@ -1614,7 +1614,7 @@ type QueryTemplates map[string]QueryTemplate ``` -### func \(QueryTemplates\) [Validate]() +### func \(QueryTemplates\) [Validate]() ```go func (t QueryTemplates) Validate() error diff --git a/internal/queries/filter_template.go b/internal/queries/filter_template.go index 046c75f7a..5122aedb8 100644 --- a/internal/queries/filter_template.go +++ b/internal/queries/filter_template.go @@ -30,7 +30,10 @@ func ValidateFilterBody(resource ResourceKind, body json.RawMessage, varDecls ma if err := unmarshalWithNumber(body, &filter); err != nil { return err } - schema := GetResourceSchema(resource) + schema, err := GetResourceSchema(resource) + if err != nil { + return err + } builder, err := query.ParseJSON(string(body)) if err != nil { @@ -125,7 +128,10 @@ func ResolveFilterTemplate(resourceKind ResourceKind, body json.RawMessage, varD } } - schema := GetResourceSchema(resourceKind) + schema, err := GetResourceSchema(resourceKind) + if err != nil { + return nil, err + } builder, err := query.ParseJSON(string(body)) if err != nil { diff --git a/internal/queries/resources.go b/internal/queries/resources.go index b43896647..21f5277e3 100644 --- a/internal/queries/resources.go +++ b/internal/queries/resources.go @@ -80,17 +80,17 @@ var VolumeSchema EntitySchema = EntitySchema{ }, } -func GetResourceSchema(kind ResourceKind) EntitySchema { +func GetResourceSchema(kind ResourceKind) (*EntitySchema, error) { switch kind { case ResourceKindAccount: - return AccountSchema + return &AccountSchema, nil case ResourceKindLog: - return LogSchema + return &LogSchema, nil case ResourceKindTransaction: - return TransactionSchema + return &TransactionSchema, nil case ResourceKindVolume: - return VolumeSchema + return &VolumeSchema, nil default: - panic(fmt.Sprintf("unexpected resources.ResourceKind: %#v", kind)) + return nil, fmt.Errorf("unexpected resources.ResourceKind: %#v", kind) } } diff --git a/internal/query_template.go b/internal/query_template.go index 432a95eec..41353d125 100644 --- a/internal/query_template.go +++ b/internal/query_template.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "slices" "strings" "github.com/iancoleman/strcase" @@ -57,6 +56,9 @@ func (p *QueryTemplateParams[Opts]) UnmarshalJSON(b []byte) error { if x.Sort != "" { parts := strings.SplitN(x.Sort, ":", 2) p.SortColumn = strcase.ToSnake(parts[0]) + if strings.TrimSpace(parts[0]) == "" { + return fmt.Errorf("invalid sort column: %q", x.Sort) + } if len(parts) > 1 { switch { case strings.ToLower(parts[1]) == "desc": @@ -104,7 +106,8 @@ type QueryTemplate struct { // Validate a query template func (q QueryTemplate) Validate() error { // check resource validity - if !slices.Contains(queries.Resources, q.Resource) { + schema, err := queries.GetResourceSchema(q.Resource) + if err != nil { return fmt.Errorf("unknown resource kind: %v", q.Resource) } // check if the params matches the resource @@ -114,6 +117,12 @@ func (q QueryTemplate) Validate() error { if err != nil { return fmt.Errorf("invalid params: %w", err) } + if params.SortColumn != "" { + _, field := schema.GetFieldByNameOrAlias(params.SortColumn) + if field == nil || !field.IsPaginated { + return fmt.Errorf("invalid sort column `%s`", params.SortColumn) + } + } switch q.Resource { case queries.ResourceKindVolume: var opts GetVolumesOptions @@ -124,7 +133,7 @@ func (q QueryTemplate) Validate() error { } } // validate variable declarations - err := queries.ValidateVarDeclarations(q.Vars) + err = queries.ValidateVarDeclarations(q.Vars) if err != nil { return fmt.Errorf("failed to validate variable declarations: %w", err) } diff --git a/internal/query_template_test.go b/internal/query_template_test.go index 4ac0e7ccc..80209cc3c 100644 --- a/internal/query_template_test.go +++ b/internal/query_template_test.go @@ -132,6 +132,26 @@ func TestQueryTemplateValidation(t *testing.T) { }`, expectedError: "cannot use variable", }, + { + name: "invalid sort column", + source: `{ + "resource": "accounts", + "params": { + "sort": "balance:asc" + } + }`, + expectedError: "invalid sort column", + }, + { + name: "invalid sort column 2", + source: `{ + "resource": "accounts", + "params": { + "sort": ":asc" + } + }`, + expectedError: "invalid sort column", + }, } { var template QueryTemplate err := unmarshalWithNumber([]byte(tc.source), &template) From 17fcb77ad95ce7aebff646d29f61cf95d65d4c28 Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Thu, 5 Feb 2026 13:58:59 +0100 Subject: [PATCH 3/9] fix migration numbers --- internal/storage/bucket/default_bucket.go | 2 +- .../notes.yaml | 0 .../{48-add-query-templates => 49-add-query-templates}/up.sql | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename internal/storage/bucket/migrations/{48-add-query-templates => 49-add-query-templates}/notes.yaml (100%) rename internal/storage/bucket/migrations/{48-add-query-templates => 49-add-query-templates}/up.sql (100%) diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 1cee4ef57..3f3a17a04 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -18,7 +18,7 @@ import ( ) // stateless version (+1 regarding directory name, as migrations start from 1 in the lib) -const MinimalSchemaVersion = 49 +const MinimalSchemaVersion = 50 type DefaultBucket struct { name string diff --git a/internal/storage/bucket/migrations/48-add-query-templates/notes.yaml b/internal/storage/bucket/migrations/49-add-query-templates/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/48-add-query-templates/notes.yaml rename to internal/storage/bucket/migrations/49-add-query-templates/notes.yaml diff --git a/internal/storage/bucket/migrations/48-add-query-templates/up.sql b/internal/storage/bucket/migrations/49-add-query-templates/up.sql similarity index 100% rename from internal/storage/bucket/migrations/48-add-query-templates/up.sql rename to internal/storage/bucket/migrations/49-add-query-templates/up.sql From 40dc2d75afe216de29f2e6f7343a54ddb5b4fa61 Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Thu, 5 Feb 2026 14:08:53 +0100 Subject: [PATCH 4/9] don't panic on empty template filters --- internal/queries/filter_template.go | 3 +++ internal/queries/filter_template_test.go | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/internal/queries/filter_template.go b/internal/queries/filter_template.go index 5122aedb8..af7b87174 100644 --- a/internal/queries/filter_template.go +++ b/internal/queries/filter_template.go @@ -137,6 +137,9 @@ func ResolveFilterTemplate(resourceKind ResourceKind, body json.RawMessage, varD if err != nil { return nil, err } + if builder == nil { + return nil, nil + } err = builder.Walk(func(operator string, key string, value *any) error { var err error diff --git a/internal/queries/filter_template_test.go b/internal/queries/filter_template_test.go index f0e891c6a..c29a9509b 100644 --- a/internal/queries/filter_template_test.go +++ b/internal/queries/filter_template_test.go @@ -134,6 +134,14 @@ func TestFilterTemplateResolution(t *testing.T) { } for _, tc := range []testCase{ + { + name: "trivial case", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `null`, + vars: map[string]any{}, + expectedFilter: `null`, + }, { name: "trivial case", resource: ResourceKindAccount, From 94156ce245d4585f25dda05115fa6a0592fb769c Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Thu, 5 Feb 2026 15:51:42 +0100 Subject: [PATCH 5/9] fix string substitution validation, fix import --- .../controller/ledger/controller_with_traces.go | 3 +-- internal/queries/filter_template.go | 15 +++++++++++---- internal/queries/filter_template_test.go | 9 +++++++++ internal/queries/substitution.go | 13 +++++++++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go index 4d206f756..02691e3d2 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -14,7 +14,6 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/queries" "github.com/formancehq/ledger/internal/storage/common" - storagecommon "github.com/formancehq/ledger/internal/storage/common" "github.com/formancehq/ledger/internal/tracing" ) @@ -590,7 +589,7 @@ func (c *ControllerWithTraces) ListSchemas(ctx context.Context, query common.Pag return schemas, nil } -func (c *ControllerWithTraces) RunQuery(ctx context.Context, schemaVersion string, id string, query common.RunQuery, paginationConfig storagecommon.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { +func (c *ControllerWithTraces) RunQuery(ctx context.Context, schemaVersion string, id string, query common.RunQuery, paginationConfig common.PaginationConfig) (*queries.ResourceKind, *bunpaginate.Cursor[any], error) { var ( resource *queries.ResourceKind cursor *bunpaginate.Cursor[any] diff --git a/internal/queries/filter_template.go b/internal/queries/filter_template.go index af7b87174..e69963ba3 100644 --- a/internal/queries/filter_template.go +++ b/internal/queries/filter_template.go @@ -80,10 +80,17 @@ func ValidateFilterBody(resource ResourceKind, body json.RawMessage, varDecls ma func validateValue(expectedType FieldType, value any, vars map[string]FieldType) error { // if value is a string and we don't expect a string, // it must be a variable placeholders that we need to validate - if valueStr, ok := value.(string); ok && (expectedType != TypeString{}) { - err := validateVarRef(expectedType, valueStr, vars) - if err != nil { - return err + if valueStr, ok := value.(string); ok { + if expectedType != (TypeString{}) { + err := validateVarRef(expectedType, valueStr, vars) + if err != nil { + return err + } + } else { + err := ValidateStringTemplate(valueStr, vars) + if err != nil { + return err + } } } else { // otherwise check that the value's type matches diff --git a/internal/queries/filter_template_test.go b/internal/queries/filter_template_test.go index c29a9509b..0c936b056 100644 --- a/internal/queries/filter_template_test.go +++ b/internal/queries/filter_template_test.go @@ -39,6 +39,15 @@ func TestFilterTemplateValidation(t *testing.T) { }}`, expectedError: "variable `doesntexist` is not declared", }, + { + name: "missing variable in interpolation", + resource: ResourceKindAccount, + varDeclarations: map[string]VarDecl{}, + source: `{"$match": { + "address": "${doesntexist}:foo" + }}`, + expectedError: "variable `doesntexist` is not declared", + }, { name: "invalid field access syntax", resource: ResourceKindAccount, diff --git a/internal/queries/substitution.go b/internal/queries/substitution.go index bfd0622a3..f447b2e86 100644 --- a/internal/queries/substitution.go +++ b/internal/queries/substitution.go @@ -9,6 +9,19 @@ import ( "strings" ) +func ValidateStringTemplate[T any](s string, vars map[string]T) error { + _, varRefs, err := ParseTemplate(s) + if err != nil { + return err + } + for _, name := range varRefs { + if _, ok := vars[name]; !ok { + return fmt.Errorf("variable `%v` is not declared", name) + } + } + return nil +} + func ReplaceVariables(s string, vars map[string]any) (string, error) { strs, varRefs, err := ParseTemplate(s) if err != nil { From 4d60404b2cd07adadcf9ea641c264813c3d060ab Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Fri, 6 Feb 2026 12:21:56 +0100 Subject: [PATCH 6/9] openapi: swap out const for singleton enums --- docs/api/README.md | 57 ++++++-- openapi.yaml | 32 +++-- openapi/v2.yaml | 32 +++-- pkg/client/.speakeasy/gen.lock | 18 ++- pkg/client/.speakeasy/logs/naming.log | 24 ++-- .../components/querytemplateaccountparams.md | 2 +- .../components/querytemplatelogparams.md | 2 +- .../querytemplatetransactionparams.md | 2 +- .../components/querytemplatevolumeparams.md | 2 +- pkg/client/docs/models/components/resource.md | 8 ++ .../components/v2accountscursorresponse.md | 2 +- .../models/components/v2logscursorresponse.md | 8 +- .../v2logscursorresponseresource.md | 8 ++ .../components/v2queryparams1resource.md | 8 ++ .../components/v2queryparams3resource.md | 8 ++ .../components/v2queryparams4resource.md | 8 ++ .../components/v2queryparamsresource.md | 8 ++ .../v2transactionscursorresponse.md | 8 +- .../v2transactionscursorresponseresource.md | 8 ++ .../v2volumeswithbalancecursorresponse.md | 8 +- ...olumeswithbalancecursorresponseresource.md | 8 ++ pkg/client/docs/sdks/v2/README.md | 1 + .../components/v2accountscursorresponse.go | 45 ++++-- .../models/components/v2logscursorresponse.go | 47 +++--- pkg/client/models/components/v2queryparams.go | 134 ++++++++++++++++-- .../v2transactionscursorresponse.go | 47 +++--- .../v2volumeswithbalancecursorresponse.go | 47 +++--- .../.speakeasy/logs/naming.log | 24 ++-- 28 files changed, 466 insertions(+), 140 deletions(-) create mode 100644 pkg/client/docs/models/components/resource.md create mode 100644 pkg/client/docs/models/components/v2logscursorresponseresource.md create mode 100644 pkg/client/docs/models/components/v2queryparams1resource.md create mode 100644 pkg/client/docs/models/components/v2queryparams3resource.md create mode 100644 pkg/client/docs/models/components/v2queryparams4resource.md create mode 100644 pkg/client/docs/models/components/v2queryparamsresource.md create mode 100644 pkg/client/docs/models/components/v2transactionscursorresponseresource.md create mode 100644 pkg/client/docs/models/components/v2volumeswithbalancecursorresponseresource.md diff --git a/docs/api/README.md b/docs/api/README.md index 085382451..bd9570a44 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -2760,6 +2760,9 @@ Format: `:`, where `` is the field name and `` is ei |Property|Value| |---|---| +|resource|transactions| +|resource|accounts| +|resource|logs| |type|NEW_TRANSACTION| |type|SET_METADATA| |type|REVERTED_TRANSACTION| @@ -2775,6 +2778,11 @@ Format: `:`, where `` is the field name and `` is ei |resource|accounts| |resource|logs| |resource|volumes| +|resource|accounts| +|resource|transactions| +|resource|logs| +|resource|volumes| +|resource|volumes|