From b25c875bdce7110ae74eb0110e72b54bb244b323 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sun, 5 Oct 2025 22:37:02 +0200 Subject: [PATCH] feat: introduce schemas --- docs/api/README.md | 299 +++++++++ internal/README.md | 124 +++- .../bulking/mocks_ledger_controller_test.go | 46 ++ .../common/mocks_ledger_controller_test.go | 46 ++ .../api/v1/mocks_ledger_controller_test.go | 46 ++ internal/api/v2/controllers_schema_insert.go | 30 + .../api/v2/controllers_schema_insert_test.go | 103 +++ internal/api/v2/controllers_schema_list.go | 48 ++ .../api/v2/controllers_schema_list_test.go | 168 +++++ internal/api/v2/controllers_schema_read.go | 28 + .../api/v2/controllers_schema_read_test.go | 101 +++ .../api/v2/mocks_ledger_controller_test.go | 46 ++ internal/api/v2/routes.go | 7 +- internal/api/v2/views.go | 4 + internal/bus/listener.go | 8 + internal/controller/ledger/controller.go | 13 +- .../controller/ledger/controller_default.go | 34 +- .../ledger/controller_generated_test.go | 46 ++ .../ledger/controller_with_events.go | 14 + ...ontroller_with_too_many_client_handling.go | 45 +- .../ledger/controller_with_traces.go | 83 +++ internal/controller/ledger/listener.go | 1 + .../ledger/listener_generated_test.go | 12 + internal/controller/ledger/store.go | 7 +- .../controller/ledger/store_generated_test.go | 45 ++ internal/log.go | 17 +- internal/schema.go | 24 + .../migrations/41-add-schema/notes.yaml | 1 + .../bucket/migrations/41-add-schema/up.sql | 14 + internal/storage/ledger/resource_schemas.go | 53 ++ internal/storage/ledger/schema.go | 39 ++ internal/storage/ledger/schema_test.go | 28 + internal/storage/ledger/store.go | 12 +- openapi.yaml | 177 +++++ openapi/v2.yaml | 177 +++++ pkg/client/.speakeasy/gen.lock | 60 +- pkg/client/.speakeasy/logs/naming.log | 15 +- pkg/client/README.md | 3 + pkg/client/docs/models/components/v2schema.md | 12 + .../docs/models/components/v2schemadata.md | 9 + .../models/components/v2schemaresponse.md | 8 + .../docs/models/components/v2schemascursor.md | 11 + .../components/v2schemascursorresponse.md | 8 + pkg/client/docs/models/operations/order.md | 9 +- .../docs/models/operations/queryparamorder.md | 10 + pkg/client/docs/models/operations/sort.md | 10 + .../models/operations/v2getschemarequest.md | 9 + .../models/operations/v2getschemaresponse.md | 9 + .../operations/v2insertschemarequest.md | 10 + .../operations/v2insertschemaresponse.md | 8 + .../models/operations/v2listschemasrequest.md | 12 + .../operations/v2listschemasresponse.md | 9 + .../operations/v2listtransactionsrequest.md | 2 +- pkg/client/docs/sdks/v2/README.md | 183 ++++++ pkg/client/models/components/v2schema.go | 50 ++ pkg/client/models/components/v2schemadata.go | 7 + .../models/components/v2schemaresponse.go | 15 + .../models/components/v2schemascursor.go | 38 ++ .../components/v2schemascursorresponse.go | 14 + pkg/client/models/operations/v2getschema.go | 48 ++ .../models/operations/v2insertschema.go | 47 ++ pkg/client/models/operations/v2listschemas.go | 140 ++++ .../models/operations/v2listtransactions.go | 20 +- .../.speakeasy/logs/naming.log | 15 +- pkg/client/v2.go | 615 ++++++++++++++++++ pkg/events/events.go | 3 +- pkg/events/message.go | 15 + test/e2e/api_schema_test.go | 144 ++++ test/e2e/api_transactions_list_test.go | 2 +- 69 files changed, 3455 insertions(+), 61 deletions(-) create mode 100644 internal/api/v2/controllers_schema_insert.go create mode 100644 internal/api/v2/controllers_schema_insert_test.go create mode 100644 internal/api/v2/controllers_schema_list.go create mode 100644 internal/api/v2/controllers_schema_list_test.go create mode 100644 internal/api/v2/controllers_schema_read.go create mode 100644 internal/api/v2/controllers_schema_read_test.go create mode 100644 internal/schema.go create mode 100644 internal/storage/bucket/migrations/41-add-schema/notes.yaml create mode 100644 internal/storage/bucket/migrations/41-add-schema/up.sql create mode 100644 internal/storage/ledger/resource_schemas.go create mode 100644 internal/storage/ledger/schema.go create mode 100644 internal/storage/ledger/schema_test.go create mode 100644 pkg/client/docs/models/components/v2schema.md create mode 100644 pkg/client/docs/models/components/v2schemadata.md create mode 100644 pkg/client/docs/models/components/v2schemaresponse.md create mode 100644 pkg/client/docs/models/components/v2schemascursor.md create mode 100644 pkg/client/docs/models/components/v2schemascursorresponse.md create mode 100644 pkg/client/docs/models/operations/queryparamorder.md create mode 100644 pkg/client/docs/models/operations/sort.md create mode 100644 pkg/client/docs/models/operations/v2getschemarequest.md create mode 100644 pkg/client/docs/models/operations/v2getschemaresponse.md create mode 100644 pkg/client/docs/models/operations/v2insertschemarequest.md create mode 100644 pkg/client/docs/models/operations/v2insertschemaresponse.md create mode 100644 pkg/client/docs/models/operations/v2listschemasrequest.md create mode 100644 pkg/client/docs/models/operations/v2listschemasresponse.md create mode 100644 pkg/client/models/components/v2schema.go create mode 100644 pkg/client/models/components/v2schemadata.go create mode 100644 pkg/client/models/components/v2schemaresponse.go create mode 100644 pkg/client/models/components/v2schemascursor.go create mode 100644 pkg/client/models/components/v2schemascursorresponse.go create mode 100644 pkg/client/models/operations/v2getschema.go create mode 100644 pkg/client/models/operations/v2insertschema.go create mode 100644 pkg/client/models/operations/v2listschemas.go create mode 100644 test/e2e/api_schema_test.go diff --git a/docs/api/README.md b/docs/api/README.md index 6996ec2e0f..30175d157e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -310,6 +310,174 @@ To perform this operation, you must be authenticated by means of one of the foll Authorization ( Scopes: ledger:write ) +## Insert or update a schema for a ledger + + + +> Code samples + +```http +POST http://localhost:8080/v2/{ledger}/schema/{version} HTTP/1.1 +Host: localhost:8080 +Content-Type: application/json +Accept: application/json + +``` + +`POST /v2/{ledger}/schema/{version}` + +> Body parameter + +```json +{} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[V2SchemaData](#schemav2schemadata)|true|none| +|ledger|path|string|true|Name of the ledger.| +|version|path|string|true|Schema version.| + +> Example responses + +> default Response + +```json +{ + "errorCode": "VALIDATION", + "errorMessage": "[VALIDATION] invalid 'cursor' query param", + "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Schema inserted successfully|None| +|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)| + + + +## Get a schema for a ledger by version + + + +> Code samples + +```http +GET http://localhost:8080/v2/{ledger}/schema/{version} HTTP/1.1 +Host: localhost:8080 +Accept: application/json + +``` + +`GET /v2/{ledger}/schema/{version}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|ledger|path|string|true|Name of the ledger.| +|version|path|string|true|Schema version.| + +> Example responses + +> 200 Response + +```json +{ + "data": { + "version": "v1.0.0", + "createdAt": "2023-01-01T00:00:00Z", + "data": {} + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Schema retrieved successfully|[V2SchemaResponse](#schemav2schemaresponse)| +|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)| + + + +## List all schemas for a ledger + + + +> Code samples + +```http +GET http://localhost:8080/v2/{ledger}/schema HTTP/1.1 +Host: localhost:8080 +Accept: application/json + +``` + +`GET /v2/{ledger}/schema` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|cursor|query|string|false|The pagination cursor value| +|pageSize|query|integer|false|The maximum number of results to return per page| +|sort|query|string|false|The field to sort by| +|order|query|string|false|The sort order| +|ledger|path|string|true|Name of the ledger.| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|sort|created_at| +|order|asc| +|order|desc| + +> Example responses + +> 200 Response + +```json +{ + "cursor": { + "data": [ + { + "version": "v1.0.0", + "createdAt": "2023-01-01T00:00:00Z", + "data": {} + } + ], + "hasMore": true, + "next": "string", + "pageSize": 0 + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Schemas retrieved successfully|[V2SchemasCursorResponse](#schemav2schemascursorresponse)| +|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)| + + + ## Update ledger metadata @@ -5235,6 +5403,137 @@ and |» errorDescription|string|true|none|none| |» errorDetails|string|false|none|none| +

V2SchemaData

+ + + + + + +```json +{} + +``` + +Schema data structure for ledger schemas + +### Properties + +*None* + +

V2Schema

+ + + + + + +```json +{ + "version": "v1.0.0", + "createdAt": "2023-01-01T00:00:00Z", + "data": {} +} + +``` + +Complete schema structure with metadata + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|true|none|Schema version| +|createdAt|string(date-time)|true|none|Schema creation timestamp| +|data|[V2SchemaData](#schemav2schemadata)|true|none|Schema data structure for ledger schemas| + +

V2SchemaResponse

+ + + + + + +```json +{ + "data": { + "version": "v1.0.0", + "createdAt": "2023-01-01T00:00:00Z", + "data": {} + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|[V2Schema](#schemav2schema)|true|none|Complete schema structure with metadata| + +

V2SchemasCursorResponse

+ + + + + + +```json +{ + "cursor": { + "data": [ + { + "version": "v1.0.0", + "createdAt": "2023-01-01T00:00:00Z", + "data": {} + } + ], + "hasMore": true, + "next": "string", + "pageSize": 0 + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|cursor|[V2SchemasCursor](#schemav2schemascursor)|true|none|none| + +

V2SchemasCursor

+ + + + + + +```json +{ + "data": [ + { + "version": "v1.0.0", + "createdAt": "2023-01-01T00:00:00Z", + "data": {} + } + ], + "hasMore": true, + "next": "string", + "pageSize": 0 +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|[[V2Schema](#schemav2schema)]|true|none|[Complete schema structure with metadata]| +|hasMore|boolean|true|none|none| +|next|string|false|none|none| +|pageSize|integer|true|none|none| +

V2CreateLedgerRequest

diff --git a/internal/README.md b/internal/README.md index 1768a8d0f4..1a6970153e 100644 --- a/internal/README.md +++ b/internal/README.md @@ -107,6 +107,9 @@ import "github.com/formancehq/ledger/internal" - [type SavedMetadata](<#SavedMetadata>) - [func \(s SavedMetadata\) Type\(\) LogType](<#SavedMetadata.Type>) - [func \(s \*SavedMetadata\) UnmarshalJSON\(data \[\]byte\) error](<#SavedMetadata.UnmarshalJSON>) +- [type Schema](<#Schema>) + - [func NewSchema\(version string, data SchemaData\) Schema](<#NewSchema>) +- [type SchemaData](<#SchemaData>) - [type Transaction](<#Transaction>) - [func NewTransaction\(\) Transaction](<#NewTransaction>) - [func \(tx Transaction\) InvolvedAccounts\(\) \[\]string](<#Transaction.InvolvedAccounts>) @@ -130,6 +133,8 @@ import "github.com/formancehq/ledger/internal" - [func NewTransactionData\(\) TransactionData](<#NewTransactionData>) - [func \(data TransactionData\) WithPostings\(postings ...Posting\) TransactionData](<#TransactionData.WithPostings>) - [type Transactions](<#Transactions>) +- [type UpdatedSchema](<#UpdatedSchema>) + - [func \(u UpdatedSchema\) Type\(\) LogType](<#UpdatedSchema.Type>) - [type Volumes](<#Volumes>) - [func NewEmptyVolumes\(\) Volumes](<#NewEmptyVolumes>) - [func NewVolumesInt64\(input, output int64\) Volumes](<#NewVolumesInt64>) @@ -191,7 +196,7 @@ var Zero = big.NewInt(0) ``` -## func [ComputeIdempotencyHash]() +## func [ComputeIdempotencyHash]() ```go func ComputeIdempotencyHash(inputs any) string @@ -273,7 +278,7 @@ func (a Account) GetAddress() string -## type [AccountMetadata]() +## type [AccountMetadata]() @@ -376,7 +381,7 @@ func (c *Configuration) Validate() error -## type [CreatedTransaction]() +## type [CreatedTransaction]() @@ -388,7 +393,7 @@ type CreatedTransaction struct { ``` -### func \(CreatedTransaction\) [GetMemento]() +### func \(CreatedTransaction\) [GetMemento]() ```go func (p CreatedTransaction) GetMemento() any @@ -397,7 +402,7 @@ func (p CreatedTransaction) GetMemento() any -### func \(CreatedTransaction\) [Type]() +### func \(CreatedTransaction\) [Type]() ```go func (p CreatedTransaction) Type() LogType @@ -406,7 +411,7 @@ func (p CreatedTransaction) Type() LogType -## type [DeletedMetadata]() +## type [DeletedMetadata]() @@ -419,7 +424,7 @@ type DeletedMetadata struct { ``` -### func \(DeletedMetadata\) [Type]() +### func \(DeletedMetadata\) [Type]() ```go func (s DeletedMetadata) Type() LogType @@ -428,7 +433,7 @@ func (s DeletedMetadata) Type() LogType -### func \(\*DeletedMetadata\) [UnmarshalJSON]() +### func \(\*DeletedMetadata\) [UnmarshalJSON]() ```go func (s *DeletedMetadata) UnmarshalJSON(data []byte) error @@ -710,7 +715,7 @@ func (l Ledger) WithMetadata(m metadata.Metadata) Ledger -## type [Log]() +## type [Log]() Log represents atomic actions made on the ledger. @@ -731,7 +736,7 @@ type Log struct { ``` -### func [NewLog]() +### func [NewLog]() ```go func NewLog(payload LogPayload) Log @@ -740,7 +745,7 @@ func NewLog(payload LogPayload) Log -### func \(Log\) [ChainLog]() +### func \(Log\) [ChainLog]() ```go func (l Log) ChainLog(previous *Log) Log @@ -749,7 +754,7 @@ func (l Log) ChainLog(previous *Log) Log -### func \(\*Log\) [ComputeHash]() +### func \(\*Log\) [ComputeHash]() ```go func (l *Log) ComputeHash(previous *Log) @@ -758,7 +763,7 @@ func (l *Log) ComputeHash(previous *Log) -### func \(\*Log\) [UnmarshalJSON]() +### func \(\*Log\) [UnmarshalJSON]() ```go func (l *Log) UnmarshalJSON(data []byte) error @@ -767,7 +772,7 @@ func (l *Log) UnmarshalJSON(data []byte) error -### func \(Log\) [WithDate]() +### func \(Log\) [WithDate]() ```go func (l Log) WithDate(date time.Time) Log @@ -776,7 +781,7 @@ func (l Log) WithDate(date time.Time) Log -### func \(Log\) [WithID]() +### func \(Log\) [WithID]() ```go func (l Log) WithID(i uint64) Log @@ -785,7 +790,7 @@ func (l Log) WithID(i uint64) Log -### func \(Log\) [WithIdempotencyKey]() +### func \(Log\) [WithIdempotencyKey]() ```go func (l Log) WithIdempotencyKey(key string) Log @@ -794,7 +799,7 @@ func (l Log) WithIdempotencyKey(key string) Log -## type [LogPayload]() +## type [LogPayload]() @@ -805,7 +810,7 @@ type LogPayload interface { ``` -### func [HydrateLog]() +### func [HydrateLog]() ```go func HydrateLog(_type LogType, data []byte) (LogPayload, error) @@ -814,7 +819,7 @@ func HydrateLog(_type LogType, data []byte) (LogPayload, error) -## type [LogType]() +## type [LogType]() @@ -830,11 +835,12 @@ const ( NewTransactionLogType // "NEW_TRANSACTION" RevertedTransactionLogType // "REVERTED_TRANSACTION" DeleteMetadataLogType + UpdatedSchemaLogType // "UPDATE_CHART_OF_ACCOUNT" ) ``` -### func [LogTypeFromString]() +### func [LogTypeFromString]() ```go func LogTypeFromString(logType string) LogType @@ -843,7 +849,7 @@ func LogTypeFromString(logType string) LogType -### func \(LogType\) [MarshalJSON]() +### func \(LogType\) [MarshalJSON]() ```go func (lt LogType) MarshalJSON() ([]byte, error) @@ -852,7 +858,7 @@ func (lt LogType) MarshalJSON() ([]byte, error) -### func \(\*LogType\) [Scan]() +### func \(\*LogType\) [Scan]() ```go func (lt *LogType) Scan(src interface{}) error @@ -861,7 +867,7 @@ func (lt *LogType) Scan(src interface{}) error -### func \(LogType\) [String]() +### func \(LogType\) [String]() ```go func (lt LogType) String() string @@ -870,7 +876,7 @@ func (lt LogType) String() string -### func \(\*LogType\) [UnmarshalJSON]() +### func \(\*LogType\) [UnmarshalJSON]() ```go func (lt *LogType) UnmarshalJSON(data []byte) error @@ -879,7 +885,7 @@ func (lt *LogType) UnmarshalJSON(data []byte) error -### func \(LogType\) [Value]() +### func \(LogType\) [Value]() ```go func (lt LogType) Value() (driver.Value, error) @@ -888,7 +894,7 @@ func (lt LogType) Value() (driver.Value, error) -## type [Memento]() +## type [Memento]() @@ -1117,7 +1123,7 @@ func (p Postings) Validate() (int, error) -## type [RevertedTransaction]() +## type [RevertedTransaction]() @@ -1129,7 +1135,7 @@ type RevertedTransaction struct { ``` -### func \(RevertedTransaction\) [GetMemento]() +### func \(RevertedTransaction\) [GetMemento]() ```go func (r RevertedTransaction) GetMemento() any @@ -1138,7 +1144,7 @@ func (r RevertedTransaction) GetMemento() any -### func \(RevertedTransaction\) [Type]() +### func \(RevertedTransaction\) [Type]() ```go func (r RevertedTransaction) Type() LogType @@ -1147,7 +1153,7 @@ func (r RevertedTransaction) Type() LogType -## type [SavedMetadata]() +## type [SavedMetadata]() @@ -1160,7 +1166,7 @@ type SavedMetadata struct { ``` -### func \(SavedMetadata\) [Type]() +### func \(SavedMetadata\) [Type]() ```go func (s SavedMetadata) Type() LogType @@ -1169,7 +1175,7 @@ func (s SavedMetadata) Type() LogType -### func \(\*SavedMetadata\) [UnmarshalJSON]() +### func \(\*SavedMetadata\) [UnmarshalJSON]() ```go func (s *SavedMetadata) UnmarshalJSON(data []byte) error @@ -1177,6 +1183,40 @@ func (s *SavedMetadata) UnmarshalJSON(data []byte) error + +## type [Schema]() + + + +```go +type Schema struct { + bun.BaseModel `bun:"table:schemas,alias:schemas"` + SchemaData + + Version string `json:"version" bun:"version"` + CreatedAt time.Time `json:"createdAt" bun:"created_at,nullzero"` +} +``` + + +### func [NewSchema]() + +```go +func NewSchema(version string, data SchemaData) Schema +``` + + + + +## type [SchemaData]() + + + +```go +type SchemaData struct { +} +``` + ## type [Transaction]() @@ -1405,6 +1445,26 @@ type Transactions struct { } ``` + +## type [UpdatedSchema]() + + + +```go +type UpdatedSchema struct { + Schema Schema `json:"schema"` +} +``` + + +### func \(UpdatedSchema\) [Type]() + +```go +func (u UpdatedSchema) Type() LogType +``` + + + ## type [Volumes]() diff --git a/internal/api/bulking/mocks_ledger_controller_test.go b/internal/api/bulking/mocks_ledger_controller_test.go index a23a9be6f4..50d9229912 100644 --- a/internal/api/bulking/mocks_ledger_controller_test.go +++ b/internal/api/bulking/mocks_ledger_controller_test.go @@ -211,6 +211,21 @@ func (mr *LedgerControllerMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*LedgerController)(nil).GetMigrationsInfo), ctx) } +// GetSchema mocks base method. +func (m *LedgerController) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSchema", ctx, version) + ret0, _ := ret[0].(*ledger.Schema) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSchema indicates an expected call of GetSchema. +func (mr *LedgerControllerMockRecorder) GetSchema(ctx, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchema", reflect.TypeOf((*LedgerController)(nil).GetSchema), ctx, version) +} + // GetStats mocks base method. func (m *LedgerController) GetStats(ctx context.Context) (ledger0.Stats, error) { m.ctrl.T.Helper() @@ -329,6 +344,21 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*LedgerController)(nil).ListLogs), ctx, query) } +// ListSchemas mocks base method. +func (m *LedgerController) ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSchemas", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Schema]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSchemas indicates an expected call of ListSchemas. +func (mr *LedgerControllerMockRecorder) ListSchemas(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSchemas", reflect.TypeOf((*LedgerController)(nil).ListSchemas), ctx, query) +} + // ListTransactions mocks base method. func (m *LedgerController) ListTransactions(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() @@ -420,3 +450,19 @@ func (mr *LedgerControllerMockRecorder) SaveTransactionMetadata(ctx, parameters mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*LedgerController)(nil).SaveTransactionMetadata), ctx, parameters) } + +// UpdateSchema mocks base method. +func (m *LedgerController) UpdateSchema(ctx context.Context, parameters ledger0.Parameters[ledger0.UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSchema", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.UpdatedSchema) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UpdateSchema indicates an expected call of UpdateSchema. +func (mr *LedgerControllerMockRecorder) UpdateSchema(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSchema", reflect.TypeOf((*LedgerController)(nil).UpdateSchema), ctx, parameters) +} diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go index 7d4dfbe96e..570d7c414a 100644 --- a/internal/api/common/mocks_ledger_controller_test.go +++ b/internal/api/common/mocks_ledger_controller_test.go @@ -211,6 +211,21 @@ func (mr *LedgerControllerMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*LedgerController)(nil).GetMigrationsInfo), ctx) } +// GetSchema mocks base method. +func (m *LedgerController) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSchema", ctx, version) + ret0, _ := ret[0].(*ledger.Schema) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSchema indicates an expected call of GetSchema. +func (mr *LedgerControllerMockRecorder) GetSchema(ctx, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchema", reflect.TypeOf((*LedgerController)(nil).GetSchema), ctx, version) +} + // GetStats mocks base method. func (m *LedgerController) GetStats(ctx context.Context) (ledger0.Stats, error) { m.ctrl.T.Helper() @@ -329,6 +344,21 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*LedgerController)(nil).ListLogs), ctx, query) } +// ListSchemas mocks base method. +func (m *LedgerController) ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSchemas", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Schema]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSchemas indicates an expected call of ListSchemas. +func (mr *LedgerControllerMockRecorder) ListSchemas(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSchemas", reflect.TypeOf((*LedgerController)(nil).ListSchemas), ctx, query) +} + // ListTransactions mocks base method. func (m *LedgerController) ListTransactions(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() @@ -420,3 +450,19 @@ func (mr *LedgerControllerMockRecorder) SaveTransactionMetadata(ctx, parameters mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*LedgerController)(nil).SaveTransactionMetadata), ctx, parameters) } + +// UpdateSchema mocks base method. +func (m *LedgerController) UpdateSchema(ctx context.Context, parameters ledger0.Parameters[ledger0.UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSchema", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.UpdatedSchema) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UpdateSchema indicates an expected call of UpdateSchema. +func (mr *LedgerControllerMockRecorder) UpdateSchema(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSchema", reflect.TypeOf((*LedgerController)(nil).UpdateSchema), ctx, parameters) +} diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go index 9497373f5d..8cc0a33a5f 100644 --- a/internal/api/v1/mocks_ledger_controller_test.go +++ b/internal/api/v1/mocks_ledger_controller_test.go @@ -211,6 +211,21 @@ func (mr *LedgerControllerMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*LedgerController)(nil).GetMigrationsInfo), ctx) } +// GetSchema mocks base method. +func (m *LedgerController) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSchema", ctx, version) + ret0, _ := ret[0].(*ledger.Schema) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSchema indicates an expected call of GetSchema. +func (mr *LedgerControllerMockRecorder) GetSchema(ctx, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchema", reflect.TypeOf((*LedgerController)(nil).GetSchema), ctx, version) +} + // GetStats mocks base method. func (m *LedgerController) GetStats(ctx context.Context) (ledger0.Stats, error) { m.ctrl.T.Helper() @@ -329,6 +344,21 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*LedgerController)(nil).ListLogs), ctx, query) } +// ListSchemas mocks base method. +func (m *LedgerController) ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSchemas", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Schema]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSchemas indicates an expected call of ListSchemas. +func (mr *LedgerControllerMockRecorder) ListSchemas(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSchemas", reflect.TypeOf((*LedgerController)(nil).ListSchemas), ctx, query) +} + // ListTransactions mocks base method. func (m *LedgerController) ListTransactions(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() @@ -420,3 +450,19 @@ func (mr *LedgerControllerMockRecorder) SaveTransactionMetadata(ctx, parameters mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*LedgerController)(nil).SaveTransactionMetadata), ctx, parameters) } + +// UpdateSchema mocks base method. +func (m *LedgerController) UpdateSchema(ctx context.Context, parameters ledger0.Parameters[ledger0.UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSchema", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.UpdatedSchema) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UpdateSchema indicates an expected call of UpdateSchema. +func (mr *LedgerControllerMockRecorder) UpdateSchema(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSchema", reflect.TypeOf((*LedgerController)(nil).UpdateSchema), ctx, parameters) +} diff --git a/internal/api/v2/controllers_schema_insert.go b/internal/api/v2/controllers_schema_insert.go new file mode 100644 index 0000000000..e45f9f6e02 --- /dev/null +++ b/internal/api/v2/controllers_schema_insert.go @@ -0,0 +1,30 @@ +package v2 + +import ( + "encoding/json" + "github.com/formancehq/go-libs/v3/api" + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/go-chi/chi/v5" + "net/http" +) + +func insertSchema(w http.ResponseWriter, r *http.Request) { + data := ledger.SchemaData{} + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } + + l := common.LedgerFromContext(r.Context()) + if _, _, err := l.UpdateSchema(r.Context(), getCommandParameters(r, ledgercontroller.UpdateSchema{ + Data: data, + Version: chi.URLParam(r, "version"), + })); err != nil { + common.HandleCommonWriteErrors(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/api/v2/controllers_schema_insert_test.go b/internal/api/v2/controllers_schema_insert_test.go new file mode 100644 index 0000000000..27bcc29de7 --- /dev/null +++ b/internal/api/v2/controllers_schema_insert_test.go @@ -0,0 +1,103 @@ +package v2 + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/go-libs/v3/auth" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "net/http" + "net/http/httptest" + "testing" +) + +func TestInsertSchema(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + version string + requestBody interface{} + expectStatusCode int + expectedErrorCode string + expectBackendCall bool + returnErr error + } + + testCases := []testCase{ + { + name: "nominal", + version: "v1.0.0", + requestBody: map[string]interface{}{ + "rules": []map[string]interface{}{ + { + "field": "postings", + "required": true, + "message": "Postings are required", + }, + }, + }, + expectStatusCode: http.StatusNoContent, + expectBackendCall: true, + }, + { + name: "empty schema data", + version: "v1.0.0", + requestBody: map[string]interface{}{}, + expectStatusCode: http.StatusNoContent, + expectBackendCall: true, + }, + { + name: "invalid JSON", + version: "v1.0.0", + requestBody: "invalid json string", + expectStatusCode: http.StatusBadRequest, + expectedErrorCode: "VALIDATION", + expectBackendCall: false, + }, + { + name: "backend error", + version: "v1.0.0", + requestBody: map[string]interface{}{ + "rules": []map[string]interface{}{}, + }, + expectStatusCode: http.StatusInternalServerError, + expectedErrorCode: "INTERNAL", + expectBackendCall: true, + returnErr: errors.New("database error"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + systemController, ledgerController := newTestingSystemController(t, true) + if tc.expectBackendCall { + ledgerController.EXPECT(). + UpdateSchema(gomock.Any(), gomock.Any()). + Return(nil, nil, tc.returnErr) + } + + router := NewRouter(systemController, auth.NewNoAuth(), "develop") + + body, _ := json.Marshal(tc.requestBody) + req := httptest.NewRequest(http.MethodPost, "/default/schema/"+tc.version, bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + require.Equal(t, tc.expectStatusCode, rec.Code) + if tc.expectedErrorCode != "" { + var errorResponse api.ErrorResponse + err := json.Unmarshal(rec.Body.Bytes(), &errorResponse) + require.NoError(t, err) + require.Equal(t, tc.expectedErrorCode, errorResponse.ErrorCode) + } + }) + } +} diff --git a/internal/api/v2/controllers_schema_list.go b/internal/api/v2/controllers_schema_list.go new file mode 100644 index 0000000000..42c59e7ce1 --- /dev/null +++ b/internal/api/v2/controllers_schema_list.go @@ -0,0 +1,48 @@ +package v2 + +import ( + "net/http" + + "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" +) + +func listSchemas(paginationConfig common.PaginationConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := common.LedgerFromContext(r.Context()) + + // Handle sort and order parameters + column := "created_at" + if sort := r.URL.Query().Get("sort"); sort != "" { + column = sort + } + + order := bunpaginate.Order(bunpaginate.OrderDesc) + if orderParam := r.URL.Query().Get("order"); orderParam != "" { + switch orderParam { + case "asc": + order = bunpaginate.Order(bunpaginate.OrderAsc) + case "desc": + order = bunpaginate.Order(bunpaginate.OrderDesc) + } + } + + query, err := getPaginatedQuery[any](r, paginationConfig, column, order) + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } + + cursor, err := l.ListSchemas(r.Context(), query) + if err != nil { + common.HandleCommonPaginationErrors(w, r, err) + return + } + + api.RenderCursor(w, *bunpaginate.MapCursor(cursor, func(schema ledger.Schema) any { + return renderSchema(r, schema) + })) + } +} diff --git a/internal/api/v2/controllers_schema_list_test.go b/internal/api/v2/controllers_schema_list_test.go new file mode 100644 index 0000000000..c9705db146 --- /dev/null +++ b/internal/api/v2/controllers_schema_list_test.go @@ -0,0 +1,168 @@ +package v2 + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/go-libs/v3/auth" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/go-libs/v3/pointer" + "github.com/formancehq/go-libs/v3/time" + ledger "github.com/formancehq/ledger/internal" + storagecommon "github.com/formancehq/ledger/internal/storage/common" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestListSchemas(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + queryParams url.Values + expectQuery storagecommon.PaginatedQuery[any] + expectStatusCode int + expectedErrorCode string + expectBackendCall bool + returnCursor *bunpaginate.Cursor[ledger.Schema] + returnErr error + } + + now := time.Now().UTC() + testSchemas := []ledger.Schema{ + { + Version: "v1.0.0", + CreatedAt: now, + SchemaData: ledger.SchemaData{}, + }, + { + Version: "v2.0.0", + CreatedAt: now.Add(time.Hour), + SchemaData: ledger.SchemaData{}, + }, + } + + testCursor := &bunpaginate.Cursor[ledger.Schema]{ + Data: testSchemas, + HasMore: false, + PageSize: 15, + } + + testCases := []testCase{ + { + name: "nominal", + queryParams: url.Values{}, + expectQuery: storagecommon.InitialPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Column: "created_at", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: storagecommon.ResourceQuery[any]{ + Expand: make([]string, 0), + }, + }, + expectStatusCode: http.StatusOK, + expectBackendCall: true, + returnCursor: testCursor, + }, + { + name: "with pagination parameters", + queryParams: url.Values{ + "pageSize": []string{"10"}, + "cursor": []string{"eyJvZmZzZXQiOjB9"}, + }, + expectQuery: storagecommon.InitialPaginatedQuery[any]{ + PageSize: 10, + Column: "created_at", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: storagecommon.ResourceQuery[any]{ + Expand: make([]string, 0), + }, + }, + expectStatusCode: http.StatusOK, + expectBackendCall: true, + returnCursor: testCursor, + }, + { + name: "with sort parameters", + queryParams: url.Values{ + "sort": []string{"created_at"}, + "order": []string{"desc"}, + }, + expectQuery: storagecommon.InitialPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Column: "created_at", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: storagecommon.ResourceQuery[any]{ + Expand: make([]string, 0), + }, + }, + expectStatusCode: http.StatusOK, + expectBackendCall: true, + returnCursor: testCursor, + }, + { + name: "backend error", + queryParams: url.Values{}, + expectQuery: storagecommon.InitialPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Column: "created_at", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: storagecommon.ResourceQuery[any]{ + Expand: make([]string, 0), + }, + }, + expectStatusCode: http.StatusInternalServerError, + expectedErrorCode: "INTERNAL", + expectBackendCall: true, + returnErr: errors.New("database error"), + }, + { + name: "invalid pagination parameters", + queryParams: url.Values{ + "pageSize": []string{"invalid"}, + }, + expectStatusCode: http.StatusBadRequest, + expectedErrorCode: "VALIDATION", + expectBackendCall: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + systemController, ledgerController := newTestingSystemController(t, true) + if tc.expectBackendCall { + ledgerController.EXPECT(). + ListSchemas(gomock.Any(), gomock.Any()). + Return(tc.returnCursor, tc.returnErr) + } + + router := NewRouter(systemController, auth.NewNoAuth(), "develop") + + req := httptest.NewRequest(http.MethodGet, "/default/schema?"+tc.queryParams.Encode(), nil) + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + require.Equal(t, tc.expectStatusCode, rec.Code) + if tc.expectedErrorCode != "" { + var errorResponse api.ErrorResponse + err := json.Unmarshal(rec.Body.Bytes(), &errorResponse) + require.NoError(t, err) + require.Equal(t, tc.expectedErrorCode, errorResponse.ErrorCode) + } else if tc.returnCursor != nil { + cursor := api.DecodeCursorResponse[ledger.Schema](t, rec.Body) + require.Len(t, cursor.Data, len(tc.returnCursor.Data)) + require.Equal(t, tc.returnCursor.HasMore, cursor.HasMore) + require.Equal(t, tc.returnCursor.PageSize, cursor.PageSize) + } + }) + } +} diff --git a/internal/api/v2/controllers_schema_read.go b/internal/api/v2/controllers_schema_read.go new file mode 100644 index 0000000000..720322cad4 --- /dev/null +++ b/internal/api/v2/controllers_schema_read.go @@ -0,0 +1,28 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/go-libs/v3/platform/postgres" + "github.com/formancehq/ledger/internal/api/common" + "github.com/go-chi/chi/v5" +) + +func readSchema(w http.ResponseWriter, r *http.Request) { + l := common.LedgerFromContext(r.Context()) + + version := chi.URLParam(r, "version") + schema, err := l.GetSchema(r.Context(), version) + if err != nil { + switch { + case postgres.IsNotFoundError(err): + api.NotFound(w, err) + default: + common.HandleCommonErrors(w, r, err) + } + return + } + + api.Ok(w, schema) +} diff --git a/internal/api/v2/controllers_schema_read_test.go b/internal/api/v2/controllers_schema_read_test.go new file mode 100644 index 0000000000..b98d0d1816 --- /dev/null +++ b/internal/api/v2/controllers_schema_read_test.go @@ -0,0 +1,101 @@ +package v2 + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/formancehq/go-libs/v3/time" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/go-libs/v3/auth" + "github.com/formancehq/go-libs/v3/platform/postgres" + ledger "github.com/formancehq/ledger/internal" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestGetSchema(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + version string + expectStatusCode int + expectedErrorCode string + expectBackendCall bool + returnSchema *ledger.Schema + returnErr error + } + + now := time.Now() + testSchema := &ledger.Schema{ + Version: "v1.0.0", + CreatedAt: now, + SchemaData: ledger.SchemaData{}, + } + + testCases := []testCase{ + { + name: "nominal", + version: "v1.0.0", + expectStatusCode: http.StatusOK, + expectBackendCall: true, + returnSchema: testSchema, + }, + { + name: "schema not found", + version: "non-existent", + expectStatusCode: http.StatusNotFound, + expectedErrorCode: "NOT_FOUND", + expectBackendCall: true, + returnErr: postgres.ErrNotFound, + }, + { + name: "backend error", + version: "v1.0.0", + expectStatusCode: http.StatusInternalServerError, + expectedErrorCode: "INTERNAL", + expectBackendCall: true, + returnErr: errors.New("database error"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + systemController, ledgerController := newTestingSystemController(t, true) + if tc.expectBackendCall { + ledgerController.EXPECT(). + GetSchema(gomock.Any(), tc.version). + Return(tc.returnSchema, tc.returnErr) + } + + router := NewRouter(systemController, auth.NewNoAuth(), "develop") + + req := httptest.NewRequest(http.MethodGet, "/default/schema/"+tc.version, nil) + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + require.Equal(t, tc.expectStatusCode, rec.Code) + if tc.expectedErrorCode != "" { + var errorResponse api.ErrorResponse + err := json.Unmarshal(rec.Body.Bytes(), &errorResponse) + require.NoError(t, err) + require.Equal(t, tc.expectedErrorCode, errorResponse.ErrorCode) + } else if tc.returnSchema != nil { + var response struct { + Data ledger.Schema `json:"data"` + } + api.Decode(t, rec.Body, &response) + require.Equal(t, tc.returnSchema.Version, response.Data.Version) + require.Equal(t, tc.returnSchema.CreatedAt, response.Data.CreatedAt) + } + }) + } +} diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go index ee0d1ce996..a1d1b68cc1 100644 --- a/internal/api/v2/mocks_ledger_controller_test.go +++ b/internal/api/v2/mocks_ledger_controller_test.go @@ -211,6 +211,21 @@ func (mr *LedgerControllerMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*LedgerController)(nil).GetMigrationsInfo), ctx) } +// GetSchema mocks base method. +func (m *LedgerController) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSchema", ctx, version) + ret0, _ := ret[0].(*ledger.Schema) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSchema indicates an expected call of GetSchema. +func (mr *LedgerControllerMockRecorder) GetSchema(ctx, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchema", reflect.TypeOf((*LedgerController)(nil).GetSchema), ctx, version) +} + // GetStats mocks base method. func (m *LedgerController) GetStats(ctx context.Context) (ledger0.Stats, error) { m.ctrl.T.Helper() @@ -329,6 +344,21 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*LedgerController)(nil).ListLogs), ctx, query) } +// ListSchemas mocks base method. +func (m *LedgerController) ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSchemas", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Schema]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSchemas indicates an expected call of ListSchemas. +func (mr *LedgerControllerMockRecorder) ListSchemas(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSchemas", reflect.TypeOf((*LedgerController)(nil).ListSchemas), ctx, query) +} + // ListTransactions mocks base method. func (m *LedgerController) ListTransactions(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() @@ -420,3 +450,19 @@ func (mr *LedgerControllerMockRecorder) SaveTransactionMetadata(ctx, parameters mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*LedgerController)(nil).SaveTransactionMetadata), ctx, parameters) } + +// UpdateSchema mocks base method. +func (m *LedgerController) UpdateSchema(ctx context.Context, parameters ledger0.Parameters[ledger0.UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSchema", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.UpdatedSchema) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UpdateSchema indicates an expected call of UpdateSchema. +func (mr *LedgerControllerMockRecorder) UpdateSchema(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSchema", reflect.TypeOf((*LedgerController)(nil).UpdateSchema), ctx, parameters) +} diff --git a/internal/api/v2/routes.go b/internal/api/v2/routes.go index b1e00b8c36..d6a1369653 100644 --- a/internal/api/v2/routes.go +++ b/internal/api/v2/routes.go @@ -1,11 +1,12 @@ package v2 import ( + "net/http" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" "github.com/formancehq/ledger/internal/api/bulking" v1 "github.com/formancehq/ledger/internal/api/v1" nooptracer "go.opentelemetry.io/otel/trace/noop" - "net/http" systemcontroller "github.com/formancehq/ledger/internal/controller/system" @@ -67,9 +68,11 @@ func NewRouter( routerOptions.bulkerFactory, routerOptions.bulkHandlerFactories, )) - router.Get("/_info", getLedgerInfo) router.Get("/stats", readStats) + router.Post("/schema/{version}", insertSchema) + router.Get("/schema/{version}", readSchema) + router.Get("/schema", listSchemas(routerOptions.paginationConfig)) if routerOptions.exporters { router.Route("/pipelines", func(router chi.Router) { diff --git a/internal/api/v2/views.go b/internal/api/v2/views.go index 01ae8d1211..9d05b34db8 100644 --- a/internal/api/v2/views.go +++ b/internal/api/v2/views.go @@ -141,6 +141,10 @@ func renderAccount(r *http.Request, v ledger.Account) any { return account(v) } +func renderSchema(r *http.Request, v ledger.Schema) any { + return v +} + type balancesByAssets ledger.BalancesByAssets func (v balancesByAssets) MarshalJSON() ([]byte, error) { diff --git a/internal/bus/listener.go b/internal/bus/listener.go index 0c77a25b8c..4dcd1b7adf 100644 --- a/internal/bus/listener.go +++ b/internal/bus/listener.go @@ -25,6 +25,14 @@ func NewLedgerListener(publisher message.Publisher) *LedgerListener { } } +func (lis *LedgerListener) UpdatedSchema(ctx context.Context, l string, data ledger.Schema) { + lis.publish(ctx, events.EventTypeUpdatedSchema, + events.NewEventUpdatedSchema(events.UpdatedSchema{ + Ledger: l, + Schema: data, + })) +} + func (lis *LedgerListener) CommittedTransactions(ctx context.Context, l string, txs ledger.Transaction, accountMetadata ledger.AccountMetadata) { lis.publish(ctx, events.EventTypeCommittedTransactions, events.NewEventCommittedTransactions(events.CommittedTransactions{ diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go index 3186b05a42..6d0b3474ad 100644 --- a/internal/controller/ledger/controller.go +++ b/internal/controller/ledger/controller.go @@ -79,6 +79,12 @@ type Controller interface { Import(ctx context.Context, stream chan ledger.Log) error // Export allow to export the logs of a ledger Export(ctx context.Context, w ExportWriter) error + // UpdateSchema Update the chart of account + UpdateSchema(ctx context.Context, parameters Parameters[UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) + // GetSchema Get the chart of account by version + 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) } type RunScript = vm.RunScript @@ -102,7 +108,7 @@ type RevertTransaction struct { Force bool AtEffectiveDate bool TransactionID uint64 - Metadata metadata.Metadata + Metadata metadata.Metadata } type SaveTransactionMetadata struct { @@ -124,3 +130,8 @@ type DeleteAccountMetadata struct { Address string Key string } + +type UpdateSchema struct { + Version string + Data ledger.SchemaData +} diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index b47caf45bd..bc5d006ff7 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -4,12 +4,14 @@ import ( "context" "database/sql" "fmt" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" "math/big" "reflect" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" + storagecommon "github.com/formancehq/ledger/internal/storage/common" + "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/pkg/features" @@ -28,7 +30,6 @@ import ( "errors" - "github.com/formancehq/go-libs/v3/bun/bunpaginate" "github.com/formancehq/go-libs/v3/logging" "github.com/formancehq/go-libs/v3/metadata" "github.com/google/uuid" @@ -56,6 +57,30 @@ type DefaultController struct { saveAccountMetadataLp *logProcessor[SaveAccountMetadata, ledger.SavedMetadata] deleteTransactionMetadataLp *logProcessor[DeleteTransactionMetadata, ledger.DeletedMetadata] deleteAccountMetadataLp *logProcessor[DeleteAccountMetadata, ledger.DeletedMetadata] + updateSchemaLp *logProcessor[UpdateSchema, ledger.UpdatedSchema] +} + +func (ctrl *DefaultController) UpdateSchema(ctx context.Context, parameters Parameters[UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + return ctrl.updateSchemaLp.forgeLog(ctx, ctrl.store, parameters, ctrl.updateSchema) +} + +func (ctrl *DefaultController) updateSchema(ctx context.Context, store Store, parameters Parameters[UpdateSchema]) (*ledger.UpdatedSchema, error) { + schema := ledger.NewSchema(parameters.Input.Version, parameters.Input.Data) + if err := store.InsertSchema(ctx, &schema); err != nil { + return nil, err + } + + return &ledger.UpdatedSchema{ + Schema: schema, + }, nil +} + +func (ctrl *DefaultController) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + return ctrl.store.FindSchema(ctx, version) +} + +func (ctrl *DefaultController) ListSchemas(ctx context.Context, query storagecommon.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + return ctrl.store.FindSchemas(ctx, query) } func (ctrl *DefaultController) Info() ledger.Ledger { @@ -135,6 +160,7 @@ func NewDefaultController( ret.saveAccountMetadataLp = newLogProcessor[SaveAccountMetadata, ledger.SavedMetadata]("SaveAccountMetadata", ret.deadLockCounter) ret.deleteTransactionMetadataLp = newLogProcessor[DeleteTransactionMetadata, ledger.DeletedMetadata]("DeleteTransactionMetadata", ret.deadLockCounter) ret.deleteAccountMetadataLp = newLogProcessor[DeleteAccountMetadata, ledger.DeletedMetadata]("DeleteAccountMetadata", ret.deadLockCounter) + ret.updateSchemaLp = newLogProcessor[UpdateSchema, ledger.UpdatedSchema]("UpdateSchema", ret.deadLockCounter) return ret } @@ -249,6 +275,10 @@ func (ctrl *DefaultController) importLog(ctx context.Context, store Store, log l "ImportLog", func(ctx context.Context) (any, error) { switch payload := log.Data.(type) { + case ledger.UpdatedSchema: + if err := store.InsertSchema(ctx, &payload.Schema); err != nil { + return nil, fmt.Errorf("failed to insert schema: %w", err) + } case ledger.CreatedTransaction: logging.FromContext(ctx).Debugf("Importing transaction %d", *payload.Transaction.ID) if err := store.CommitTransaction(ctx, &payload.Transaction, payload.AccountMetadata); err != nil { diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go index fbcaefe97e..0084d55b0b 100644 --- a/internal/controller/ledger/controller_generated_test.go +++ b/internal/controller/ledger/controller_generated_test.go @@ -210,6 +210,21 @@ func (mr *MockControllerMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*MockController)(nil).GetMigrationsInfo), ctx) } +// GetSchema mocks base method. +func (m *MockController) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSchema", ctx, version) + ret0, _ := ret[0].(*ledger.Schema) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSchema indicates an expected call of GetSchema. +func (mr *MockControllerMockRecorder) GetSchema(ctx, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchema", reflect.TypeOf((*MockController)(nil).GetSchema), ctx, version) +} + // GetStats mocks base method. func (m *MockController) GetStats(ctx context.Context) (Stats, error) { m.ctrl.T.Helper() @@ -328,6 +343,21 @@ func (mr *MockControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*MockController)(nil).ListLogs), ctx, query) } +// ListSchemas mocks base method. +func (m *MockController) ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSchemas", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Schema]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSchemas indicates an expected call of ListSchemas. +func (mr *MockControllerMockRecorder) ListSchemas(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSchemas", reflect.TypeOf((*MockController)(nil).ListSchemas), ctx, query) +} + // ListTransactions mocks base method. func (m *MockController) ListTransactions(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() @@ -419,3 +449,19 @@ func (mr *MockControllerMockRecorder) SaveTransactionMetadata(ctx, parameters an mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*MockController)(nil).SaveTransactionMetadata), ctx, parameters) } + +// UpdateSchema mocks base method. +func (m *MockController) UpdateSchema(ctx context.Context, parameters Parameters[UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSchema", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.UpdatedSchema) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UpdateSchema indicates an expected call of UpdateSchema. +func (mr *MockControllerMockRecorder) UpdateSchema(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSchema", reflect.TypeOf((*MockController)(nil).UpdateSchema), ctx, parameters) +} diff --git a/internal/controller/ledger/controller_with_events.go b/internal/controller/ledger/controller_with_events.go index 600d62eb9a..cbde076d65 100644 --- a/internal/controller/ledger/controller_with_events.go +++ b/internal/controller/ledger/controller_with_events.go @@ -151,6 +151,20 @@ func (c *ControllerWithEvents) DeleteAccountMetadata(ctx context.Context, parame return log, nil } +func (c *ControllerWithEvents) UpdateSchema(ctx context.Context, parameters Parameters[UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + log, ret, err := c.Controller.UpdateSchema(ctx, parameters) + if err != nil { + return nil, nil, err + } + if !parameters.DryRun { + c.handleEvent(ctx, func() { + c.listener.UpdatedSchema(ctx, c.ledger.Name, ret.Schema) + }) + } + + return log, ret, nil +} + func (c *ControllerWithEvents) 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_too_many_client_handling.go b/internal/controller/ledger/controller_with_too_many_client_handling.go index 9ec23edfbe..f946be1143 100644 --- a/internal/controller/ledger/controller_with_too_many_client_handling.go +++ b/internal/controller/ledger/controller_with_too_many_client_handling.go @@ -4,12 +4,15 @@ import ( "context" "database/sql" "errors" + "time" + + "github.com/formancehq/go-libs/v3/bun/bunpaginate" "github.com/formancehq/go-libs/v3/platform/postgres" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/storage/common" "github.com/uptrace/bun" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "time" ) //go:generate mockgen -write_source_comment=false -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 -typed @@ -119,6 +122,46 @@ func (c *ControllerWithTooManyClientHandling) DeleteAccountMetadata(ctx context. return log, err } +func (c *ControllerWithTooManyClientHandling) UpdateSchema(ctx context.Context, parameters Parameters[UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + var ( + log *ledger.Log + err error + ret *ledger.UpdatedSchema + ) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + log, ret, err = c.Controller.UpdateSchema(ctx, parameters) + return err + }) + + return log, ret, err +} + +func (c *ControllerWithTooManyClientHandling) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + var ( + schema *ledger.Schema + err error + ) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + schema, err = c.Controller.GetSchema(ctx, version) + return err + }) + + return schema, err +} + +func (c *ControllerWithTooManyClientHandling) ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + var ( + schemas *bunpaginate.Cursor[ledger.Schema] + err error + ) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + schemas, err = c.Controller.ListSchemas(ctx, query) + return err + }) + + return schemas, 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 2dbcea0001..846a43d55c 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -3,6 +3,7 @@ package ledger import ( "context" "database/sql" + "github.com/formancehq/go-libs/v3/migrations" "github.com/formancehq/ledger/internal/storage/common" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" @@ -42,6 +43,9 @@ type ControllerWithTraces struct { deleteTransactionMetadataHistogram metric.Int64Histogram deleteAccountMetadataHistogram metric.Int64Histogram lockLedgerHistogram metric.Int64Histogram + updateSchemaHistogram metric.Int64Histogram + getSchemaHistogram metric.Int64Histogram + listSchemasHistogram metric.Int64Histogram } func (c *ControllerWithTraces) Info() ledger.Ledger { @@ -147,6 +151,18 @@ func NewControllerWithTraces(underlying Controller, tracer trace.Tracer, meter m if err != nil { panic(err) } + ret.updateSchemaHistogram, err = meter.Int64Histogram("controller.update_schema") + if err != nil { + panic(err) + } + ret.getSchemaHistogram, err = meter.Int64Histogram("controller.get_schema") + if err != nil { + panic(err) + } + ret.listSchemasHistogram, err = meter.Int64Histogram("controller.list_schemas") + if err != nil { + panic(err) + } return ret } @@ -454,6 +470,73 @@ func (c *ControllerWithTraces) DeleteAccountMetadata(ctx context.Context, parame ) } +func (c *ControllerWithTraces) UpdateSchema(ctx context.Context, parameters Parameters[UpdateSchema]) (*ledger.Log, *ledger.UpdatedSchema, error) { + var ( + updatedSchema *ledger.UpdatedSchema + log *ledger.Log + err error + ) + _, err = tracing.TraceWithMetric( + ctx, + "UpdatedSchema", + c.tracer, + c.updateSchemaHistogram, + func(ctx context.Context) (any, error) { + log, updatedSchema, err = c.underlying.UpdateSchema(ctx, parameters) + return nil, err + }, + ) + if err != nil { + return nil, nil, err + } + + return log, updatedSchema, nil +} + +func (c *ControllerWithTraces) GetSchema(ctx context.Context, version string) (*ledger.Schema, error) { + var ( + schema *ledger.Schema + err error + ) + _, err = tracing.TraceWithMetric( + ctx, + "GetSchema", + c.tracer, + c.getSchemaHistogram, + func(ctx context.Context) (any, error) { + schema, err = c.underlying.GetSchema(ctx, version) + return nil, err + }, + ) + if err != nil { + return nil, err + } + + return schema, nil +} + +func (c *ControllerWithTraces) ListSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + var ( + schemas *bunpaginate.Cursor[ledger.Schema] + err error + ) + _, err = tracing.TraceWithMetric( + ctx, + "ListSchemas", + c.tracer, + c.listSchemasHistogram, + func(ctx context.Context) (any, error) { + schemas, err = c.underlying.ListSchemas(ctx, query) + return nil, err + }, + ) + if err != nil { + return nil, err + } + + return schemas, nil +} + func (c *ControllerWithTraces) GetStats(ctx context.Context) (Stats, error) { return tracing.TraceWithMetric( ctx, diff --git a/internal/controller/ledger/listener.go b/internal/controller/ledger/listener.go index b8d6ec414a..1fcc9c9da9 100644 --- a/internal/controller/ledger/listener.go +++ b/internal/controller/ledger/listener.go @@ -13,4 +13,5 @@ type Listener interface { SavedMetadata(ctx context.Context, ledger string, targetType, id string, metadata metadata.Metadata) RevertedTransaction(ctx context.Context, ledger string, reverted, revert ledger.Transaction) DeletedMetadata(ctx context.Context, ledger string, targetType string, targetID any, key string) + UpdatedSchema(ctx context.Context, ledger string, data ledger.Schema) } diff --git a/internal/controller/ledger/listener_generated_test.go b/internal/controller/ledger/listener_generated_test.go index 127408d0e2..44e8c89e40 100644 --- a/internal/controller/ledger/listener_generated_test.go +++ b/internal/controller/ledger/listener_generated_test.go @@ -87,3 +87,15 @@ func (mr *MockListenerMockRecorder) SavedMetadata(ctx, arg1, targetType, id, arg mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavedMetadata", reflect.TypeOf((*MockListener)(nil).SavedMetadata), ctx, arg1, targetType, id, arg4) } + +// UpdatedSchema mocks base method. +func (m *MockListener) UpdatedSchema(ctx context.Context, arg1 string, data ledger.Schema) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatedSchema", ctx, arg1, data) +} + +// UpdatedSchema indicates an expected call of UpdatedSchema. +func (mr *MockListenerMockRecorder) UpdatedSchema(ctx, arg1, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedSchema", reflect.TypeOf((*MockListener)(nil).UpdatedSchema), ctx, arg1, data) +} diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go index 70fe93b110..a8569b32e9 100644 --- a/internal/controller/ledger/store.go +++ b/internal/controller/ledger/store.go @@ -3,10 +3,11 @@ package ledger import ( "context" "database/sql" + "math/big" + "github.com/formancehq/ledger/internal/storage/common" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" "github.com/uptrace/bun" - "math/big" "github.com/formancehq/go-libs/v3/migrations" "github.com/formancehq/numscript" @@ -16,6 +17,7 @@ import ( "github.com/formancehq/go-libs/v3/time" ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/machine/vm" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" ) type Balance struct { @@ -44,6 +46,9 @@ type Store interface { // UpsertAccount returns a boolean indicating if the account was upserted UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error DeleteAccountMetadata(ctx context.Context, address, key string) error + InsertSchema(ctx context.Context, data *ledger.Schema) error + FindSchema(ctx context.Context, version string) (*ledger.Schema, error) + FindSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) InsertLog(ctx context.Context, log *ledger.Log) error LockLedger(ctx context.Context) (Store, bun.IDB, func() error, error) diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 17bb721ed0..f0163bc6ae 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -12,6 +12,7 @@ import ( sql "database/sql" reflect "reflect" + bunpaginate "github.com/formancehq/go-libs/v3/bun/bunpaginate" metadata "github.com/formancehq/go-libs/v3/metadata" migrations "github.com/formancehq/go-libs/v3/migrations" time "github.com/formancehq/go-libs/v3/time" @@ -148,6 +149,36 @@ func (mr *MockStoreMockRecorder) DeleteTransactionMetadata(ctx, transactionID, k return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionMetadata", reflect.TypeOf((*MockStore)(nil).DeleteTransactionMetadata), ctx, transactionID, key, at) } +// FindSchema mocks base method. +func (m *MockStore) FindSchema(ctx context.Context, version string) (*ledger.Schema, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindSchema", ctx, version) + ret0, _ := ret[0].(*ledger.Schema) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindSchema indicates an expected call of FindSchema. +func (mr *MockStoreMockRecorder) FindSchema(ctx, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindSchema", reflect.TypeOf((*MockStore)(nil).FindSchema), ctx, version) +} + +// FindSchemas mocks base method. +func (m *MockStore) FindSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindSchemas", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Schema]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindSchemas indicates an expected call of FindSchemas. +func (mr *MockStoreMockRecorder) FindSchemas(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindSchemas", reflect.TypeOf((*MockStore)(nil).FindSchemas), ctx, query) +} + // GetBalances mocks base method. func (m *MockStore) GetBalances(ctx context.Context, query ledger0.BalanceQuery) (ledger.Balances, error) { m.ctrl.T.Helper() @@ -192,6 +223,20 @@ func (mr *MockStoreMockRecorder) InsertLog(ctx, log any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertLog", reflect.TypeOf((*MockStore)(nil).InsertLog), ctx, log) } +// InsertSchema mocks base method. +func (m *MockStore) InsertSchema(ctx context.Context, data *ledger.Schema) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertSchema", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertSchema indicates an expected call of InsertSchema. +func (mr *MockStoreMockRecorder) InsertSchema(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertSchema", reflect.TypeOf((*MockStore)(nil).InsertSchema), ctx, data) +} + // IsUpToDate mocks base method. func (m *MockStore) IsUpToDate(ctx context.Context) (bool, error) { m.ctrl.T.Helper() diff --git a/internal/log.go b/internal/log.go index 958cacd4ab..c0febdd573 100644 --- a/internal/log.go +++ b/internal/log.go @@ -22,6 +22,7 @@ const ( NewTransactionLogType // "NEW_TRANSACTION" RevertedTransactionLogType // "REVERTED_TRANSACTION" DeleteMetadataLogType + UpdatedSchemaLogType // "UPDATE_CHART_OF_ACCOUNT" ) type LogType int16 @@ -60,9 +61,11 @@ func (lt LogType) String() string { return "REVERTED_TRANSACTION" case DeleteMetadataLogType: return "DELETE_METADATA" + case UpdatedSchemaLogType: + return "UPDATED_SCHEMA" } - return "" + panic("invalid log type") } func LogTypeFromString(logType string) LogType { @@ -75,6 +78,8 @@ func LogTypeFromString(logType string) LogType { return RevertedTransactionLogType case "DELETE_METADATA": return DeleteMetadataLogType + case "UPDATED_SCHEMA": + return UpdatedSchemaLogType } panic("invalid log type") @@ -365,6 +370,14 @@ func (r RevertedTransaction) GetMemento() any { var _ Memento = (*RevertedTransaction)(nil) +type UpdatedSchema struct { + Schema Schema `json:"schema"` +} + +func (u UpdatedSchema) Type() LogType { + return UpdatedSchemaLogType +} + func HydrateLog(_type LogType, data []byte) (LogPayload, error) { var payload any switch _type { @@ -376,6 +389,8 @@ func HydrateLog(_type LogType, data []byte) (LogPayload, error) { payload = &DeletedMetadata{} case RevertedTransactionLogType: payload = &RevertedTransaction{} + case UpdatedSchemaLogType: + payload = &UpdatedSchema{} default: return nil, fmt.Errorf("unknown type '%s'", _type) } diff --git a/internal/schema.go b/internal/schema.go new file mode 100644 index 0000000000..80c55301d1 --- /dev/null +++ b/internal/schema.go @@ -0,0 +1,24 @@ +package ledger + +import ( + "github.com/formancehq/go-libs/v3/time" + "github.com/uptrace/bun" +) + +type SchemaData struct { +} + +type Schema struct { + bun.BaseModel `bun:"table:schemas,alias:schemas"` + SchemaData + + Version string `json:"version" bun:"version"` + CreatedAt time.Time `json:"createdAt" bun:"created_at,nullzero"` +} + +func NewSchema(version string, data SchemaData) Schema { + return Schema{ + Version: version, + SchemaData: data, + } +} diff --git a/internal/storage/bucket/migrations/41-add-schema/notes.yaml b/internal/storage/bucket/migrations/41-add-schema/notes.yaml new file mode 100644 index 0000000000..385db814b3 --- /dev/null +++ b/internal/storage/bucket/migrations/41-add-schema/notes.yaml @@ -0,0 +1 @@ +name: Add schema \ No newline at end of file diff --git a/internal/storage/bucket/migrations/41-add-schema/up.sql b/internal/storage/bucket/migrations/41-add-schema/up.sql new file mode 100644 index 0000000000..b8c32d1229 --- /dev/null +++ b/internal/storage/bucket/migrations/41-add-schema/up.sql @@ -0,0 +1,14 @@ +do $$ + begin + set search_path = '{{ .Schema }}'; + + create table schemas ( + ledger varchar, + version text not null, + created_at timestamp without time zone not null default now(), + primary key (ledger, version) + ); + + alter type log_type add value 'UPDATED_SCHEMA'; + end +$$; \ No newline at end of file diff --git a/internal/storage/ledger/resource_schemas.go b/internal/storage/ledger/resource_schemas.go new file mode 100644 index 0000000000..0fe6f69739 --- /dev/null +++ b/internal/storage/ledger/resource_schemas.go @@ -0,0 +1,53 @@ +package ledger + +import ( + "errors" + "fmt" + + "github.com/formancehq/ledger/internal/storage/common" + "github.com/uptrace/bun" +) + +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) BuildDataset(opts common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { + q := h.store.db.NewSelect(). + ModelTableExpr(h.store.GetPrefixedRelationName("schemas")). + Where("ledger = ?", h.store.ledger.Name) + + if opts.PIT != nil && !opts.PIT.IsZero() { + q = q.Where("created_at <= ?", opts.PIT) + } + + return q, nil +} + +func (h schemasResourceHandler) Project(_ common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { + return selectQuery.ColumnExpr("*"), nil +} + +func (h schemasResourceHandler) ResolveFilter(_ common.ResourceQuery[any], operator, property string, value any) (string, []any, error) { + switch property { + case "version", "created_at": + return fmt.Sprintf("%s %s ?", property, common.ConvertOperatorToSQL(operator)), []any{value}, nil + default: + return "", nil, fmt.Errorf("unknown key '%s' when building query", property) + } +} + +func (h schemasResourceHandler) Expand(_ common.ResourceQuery[any], _ string) (*bun.SelectQuery, *common.JoinCondition, error) { + return nil, nil, errors.New("no expand supported") +} + +var _ common.RepositoryHandler[any] = schemasResourceHandler{} diff --git a/internal/storage/ledger/schema.go b/internal/storage/ledger/schema.go new file mode 100644 index 0000000000..5d669afd8e --- /dev/null +++ b/internal/storage/ledger/schema.go @@ -0,0 +1,39 @@ +package ledger + +import ( + "context" + + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/go-libs/v3/platform/postgres" + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/storage/common" +) + +func (s *Store) InsertSchema(ctx context.Context, schema *ledger.Schema) error { + _, err := s.db.NewInsert(). + Model(schema). + Value("ledger", "?", s.ledger.Name). + ModelTableExpr(s.GetPrefixedRelationName("schemas")). + Returning("created_at"). + Exec(ctx) + return postgres.ResolveError(err) +} + +func (s *Store) FindSchema(ctx context.Context, version string) (*ledger.Schema, error) { + schema := &ledger.Schema{} + err := s.db.NewSelect(). + Model(schema). + ModelTableExpr(s.GetPrefixedRelationName("schemas")). + Where("version = ?", version). + Where("ledger = ?", s.ledger.Name). + Scan(ctx) + if err != nil { + return nil, postgres.ResolveError(err) + } + + return schema, nil +} + +func (s *Store) FindSchemas(ctx context.Context, query common.PaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Schema], error) { + return s.Schemas().Paginate(ctx, query) +} diff --git a/internal/storage/ledger/schema_test.go b/internal/storage/ledger/schema_test.go new file mode 100644 index 0000000000..83b776ad3c --- /dev/null +++ b/internal/storage/ledger/schema_test.go @@ -0,0 +1,28 @@ +//go:build it + +package ledger_test + +import ( + "testing" + + "github.com/formancehq/go-libs/v3/logging" + + ledger "github.com/formancehq/ledger/internal" + "github.com/stretchr/testify/require" +) + +func TestSchemaUpdate(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + + store := newLedgerStore(t) + + schema := ledger.NewSchema("1.0", ledger.SchemaData{ + + }) + err := store.InsertSchema(ctx, &schema) + require.NoError(t, err) + require.Equal(t, "1.0", schema.Version) + require.NotZero(t, schema.CreatedAt) +} diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index fed1ffb2f5..9c38182861 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" "github.com/formancehq/go-libs/v3/migrations" "github.com/formancehq/go-libs/v3/platform/postgres" @@ -16,6 +17,7 @@ import ( nooptracer "go.opentelemetry.io/otel/trace/noop" "errors" + "github.com/uptrace/bun" ) @@ -80,6 +82,14 @@ func (store *Store) Accounts() common.PaginatedResource[ }, "address", bunpaginate.OrderAsc) } +func (store *Store) Schemas() common.PaginatedResource[ + ledger.Schema, + any] { + return common.NewPaginatedResourceRepository[ledger.Schema, any](&schemasResourceHandler{ + store: store, + }, "created_at", bunpaginate.OrderDesc) +} + func (store *Store) BeginTX(ctx context.Context, options *sql.TxOptions) (*Store, *bun.Tx, error) { tx, err := store.db.BeginTx(ctx, options) if err != nil { @@ -174,7 +184,7 @@ func New(db bun.IDB, bucket bucket.Bucket, l ledger.Ledger, opts ...Option) *Sto } var err error - ret.checkBucketSchemaHistogram, err = ret.meter.Int64Histogram("store.check_bucket_schema") + ret.checkBucketSchemaHistogram, err = ret.meter.Int64Histogram("store.check_bucket_schema") if err != nil { panic(err) } diff --git a/openapi.yaml b/openapi.yaml index e31ce9de2e..7c847a10db 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1431,6 +1431,130 @@ paths: security: - Authorization: - ledger:write + /v2/{ledger}/schema/{version}: + parameters: + - name: ledger + in: path + description: Name of the ledger. + required: true + schema: + type: string + example: ledger001 + - name: version + in: path + description: Schema version. + required: true + schema: + type: string + example: v1.0.0 + post: + summary: Insert or update a schema for a ledger + operationId: v2InsertSchema + x-speakeasy-name-override: InsertSchema + tags: + - ledger.v2 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/V2SchemaData" + responses: + "204": + description: Schema inserted successfully + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:write + get: + summary: Get a schema for a ledger by version + operationId: v2GetSchema + x-speakeasy-name-override: GetSchema + tags: + - ledger.v2 + responses: + "200": + description: Schema retrieved successfully + content: + application/json: + schema: + $ref: "#/components/schemas/V2SchemaResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:read + /v2/{ledger}/schema: + parameters: + - name: ledger + in: path + description: Name of the ledger. + required: true + schema: + type: string + example: ledger001 + get: + summary: List all schemas for a ledger + operationId: v2ListSchemas + x-speakeasy-name-override: ListSchemas + tags: + - ledger.v2 + parameters: + - name: cursor + in: query + description: The pagination cursor value + schema: + type: string + - name: pageSize + in: query + description: The maximum number of results to return per page + schema: + type: integer + minimum: 1 + maximum: 1000 + default: 15 + - name: sort + in: query + description: The field to sort by + schema: + type: string + enum: + - created_at + default: created_at + - name: order + in: query + description: The sort order + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + "200": + description: Schemas retrieved successfully + content: + application/json: + schema: + $ref: "#/components/schemas/V2SchemasCursorResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:read /v2/{ledger}/metadata: parameters: - name: ledger @@ -4215,6 +4339,59 @@ components: required: - errorCode - errorDescription + V2SchemaData: + type: object + description: Schema data structure for ledger schemas + V2Schema: + type: object + description: Complete schema structure with metadata + properties: + version: + type: string + description: Schema version + example: "v1.0.0" + createdAt: + type: string + format: date-time + description: Schema creation timestamp + example: "2023-01-01T00:00:00Z" + data: + $ref: "#/components/schemas/V2SchemaData" + required: + - version + - createdAt + - data + V2SchemaResponse: + properties: + data: + $ref: "#/components/schemas/V2Schema" + type: object + required: + - data + V2SchemasCursorResponse: + properties: + cursor: + $ref: "#/components/schemas/V2SchemasCursor" + type: object + required: + - cursor + V2SchemasCursor: + properties: + data: + type: array + items: + $ref: "#/components/schemas/V2Schema" + hasMore: + type: boolean + next: + type: string + pageSize: + type: integer + type: object + required: + - data + - hasMore + - pageSize V2CreateLedgerRequest: type: object properties: diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 3a3011cf59..d3e6f132c5 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -170,6 +170,130 @@ paths: security: - Authorization: - ledger:write + /v2/{ledger}/schema/{version}: + parameters: + - name: ledger + in: path + description: Name of the ledger. + required: true + schema: + type: string + example: ledger001 + - name: version + in: path + description: Schema version. + required: true + schema: + type: string + example: v1.0.0 + post: + summary: Insert or update a schema for a ledger + operationId: v2InsertSchema + x-speakeasy-name-override: InsertSchema + tags: + - ledger.v2 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/V2SchemaData" + responses: + "204": + description: Schema inserted successfully + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:write + get: + summary: Get a schema for a ledger by version + operationId: v2GetSchema + x-speakeasy-name-override: GetSchema + tags: + - ledger.v2 + responses: + "200": + description: Schema retrieved successfully + content: + application/json: + schema: + $ref: "#/components/schemas/V2SchemaResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:read + /v2/{ledger}/schema: + parameters: + - name: ledger + in: path + description: Name of the ledger. + required: true + schema: + type: string + example: ledger001 + get: + summary: List all schemas for a ledger + operationId: v2ListSchemas + x-speakeasy-name-override: ListSchemas + tags: + - ledger.v2 + parameters: + - name: cursor + in: query + description: The pagination cursor value + schema: + type: string + - name: pageSize + in: query + description: The maximum number of results to return per page + schema: + type: integer + minimum: 1 + maximum: 1000 + default: 15 + - name: sort + in: query + description: The field to sort by + schema: + type: string + enum: + - created_at + default: created_at + - name: order + in: query + description: The sort order + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + "200": + description: Schemas retrieved successfully + content: + application/json: + schema: + $ref: "#/components/schemas/V2SchemasCursorResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:read /v2/{ledger}/metadata: parameters: - name: ledger @@ -2461,6 +2585,59 @@ components: required: - errorCode - errorDescription + V2SchemaData: + type: object + description: Schema data structure for ledger schemas + V2Schema: + type: object + description: Complete schema structure with metadata + properties: + version: + type: string + description: Schema version + example: "v1.0.0" + createdAt: + type: string + format: date-time + description: Schema creation timestamp + example: "2023-01-01T00:00:00Z" + data: + $ref: "#/components/schemas/V2SchemaData" + required: + - version + - createdAt + - data + V2SchemaResponse: + properties: + data: + $ref: "#/components/schemas/V2Schema" + type: object + required: + - data + V2SchemasCursorResponse: + properties: + cursor: + $ref: "#/components/schemas/V2SchemasCursor" + type: object + required: + - cursor + V2SchemasCursor: + properties: + data: + type: array + items: + $ref: "#/components/schemas/V2Schema" + hasMore: + type: boolean + next: + type: string + pageSize: + type: integer + type: object + required: + - data + - hasMore + - pageSize V2CreateLedgerRequest: type: object properties: diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index ed8722a21f..4a41132596 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: 679d4d7c73e2d08f97b9358d13b6bb68 + docChecksum: 920ba5d55ce048f2654006c5aa63a8d5 docVersion: v2 speakeasyVersion: 1.563.0 generationVersion: 2.629.1 @@ -11,6 +11,7 @@ features: go: additionalDependencies: 0.1.0 bigint: 0.0.2 + constsAndDefaults: 0.1.12 core: 3.8.1 defaultEnabledRetries: 0.2.0 deprecations: 2.81.3 @@ -109,6 +110,11 @@ generatedFiles: - /models/components/v2posting.go - /models/components/v2posttransaction.go - /models/components/v2reverttransactionrequest.go + - /models/components/v2schema.go + - /models/components/v2schemadata.go + - /models/components/v2schemaresponse.go + - /models/components/v2schemascursor.go + - /models/components/v2schemascursorresponse.go - /models/components/v2stats.go - /models/components/v2statsresponse.go - /models/components/v2targetid.go @@ -162,14 +168,17 @@ generatedFiles: - /models/operations/v2getledger.go - /models/operations/v2getledgerinfo.go - /models/operations/v2getpipelinestate.go + - /models/operations/v2getschema.go - /models/operations/v2gettransaction.go - /models/operations/v2getvolumeswithbalances.go - /models/operations/v2importlogs.go + - /models/operations/v2insertschema.go - /models/operations/v2listaccounts.go - /models/operations/v2listexporters.go - /models/operations/v2listledgers.go - /models/operations/v2listlogs.go - /models/operations/v2listpipelines.go + - /models/operations/v2listschemas.go - /models/operations/v2listtransactions.go - /models/operations/v2readstats.go - /models/operations/v2resetpipeline.go @@ -281,6 +290,11 @@ generatedFiles: - docs/models/components/v2posttransaction.md - docs/models/components/v2posttransactionscript.md - docs/models/components/v2reverttransactionrequest.md + - docs/models/components/v2schema.md + - docs/models/components/v2schemadata.md + - docs/models/components/v2schemaresponse.md + - docs/models/components/v2schemascursor.md + - docs/models/components/v2schemascursorresponse.md - docs/models/components/v2stats.md - docs/models/components/v2statsresponse.md - docs/models/components/v2targetid.md @@ -328,12 +342,14 @@ generatedFiles: - docs/models/operations/metadata.md - docs/models/operations/option.md - docs/models/operations/order.md + - docs/models/operations/queryparamorder.md - docs/models/operations/readstatsrequest.md - docs/models/operations/readstatsresponse.md - docs/models/operations/reverttransactionrequest.md - docs/models/operations/reverttransactionresponse.md - docs/models/operations/runscriptrequest.md - docs/models/operations/runscriptresponse.md + - docs/models/operations/sort.md - docs/models/operations/updatemappingrequest.md - docs/models/operations/updatemappingresponse.md - docs/models/operations/v2addmetadataontransactionrequest.md @@ -378,12 +394,16 @@ generatedFiles: - docs/models/operations/v2getledgerresponse.md - docs/models/operations/v2getpipelinestaterequest.md - docs/models/operations/v2getpipelinestateresponse.md + - docs/models/operations/v2getschemarequest.md + - docs/models/operations/v2getschemaresponse.md - docs/models/operations/v2gettransactionrequest.md - docs/models/operations/v2gettransactionresponse.md - docs/models/operations/v2getvolumeswithbalancesrequest.md - docs/models/operations/v2getvolumeswithbalancesresponse.md - docs/models/operations/v2importlogsrequest.md - docs/models/operations/v2importlogsresponse.md + - docs/models/operations/v2insertschemarequest.md + - docs/models/operations/v2insertschemaresponse.md - docs/models/operations/v2listaccountsrequest.md - docs/models/operations/v2listaccountsresponse.md - docs/models/operations/v2listexportersresponse.md @@ -393,6 +413,8 @@ generatedFiles: - docs/models/operations/v2listlogsresponse.md - docs/models/operations/v2listpipelinesrequest.md - docs/models/operations/v2listpipelinesresponse.md + - docs/models/operations/v2listschemasrequest.md + - docs/models/operations/v2listschemasresponse.md - docs/models/operations/v2listtransactionsrequest.md - docs/models/operations/v2listtransactionsresponse.md - docs/models/operations/v2readstatsrequest.md @@ -1138,5 +1160,41 @@ examples: responses: default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} + v2InsertSchema: + speakeasy-default-v2-insert-schema: + parameters: + path: + ledger: "ledger001" + version: "v1.0.0" + requestBody: + application/json: {} + responses: + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} + v2GetSchema: + speakeasy-default-v2-get-schema: + parameters: + path: + ledger: "ledger001" + version: "v1.0.0" + responses: + "200": + application/json: {"data": {"version": "v1.0.0", "createdAt": "2023-01-01T00:00:00Z", "data": {}}} + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"} + v2ListSchemas: + speakeasy-default-v2-list-schemas: + parameters: + path: + ledger: "ledger001" + query: + pageSize: 15 + sort: "created_at" + order: "desc" + responses: + "200": + application/json: {"cursor": {"data": [], "hasMore": true, "pageSize": 47856}} + 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 5da1bd87e5..d569ce5b48 100644 --- a/pkg/client/.speakeasy/logs/naming.log +++ b/pkg/client/.speakeasy/logs/naming.log @@ -101,6 +101,19 @@ V2GetLedgerResponse (HttpMeta: HTTPMetadata, V2GetLedgerResponse: V2GetLedgerRes V2CreateLedgerRequest (ledger: string, V2CreateLedgerRequest: V2CreateLedgerRequest) V2CreateLedgerRequest (bucket: string, metadata: map, features: map) V2CreateLedgerResponse (HttpMeta: HTTPMetadata) +V2InsertSchemaRequest (ledger: string, version: string, V2SchemaData: V2SchemaData) + V2SchemaData (empty) +V2InsertSchemaResponse (HttpMeta: HTTPMetadata) +V2GetSchemaRequest (ledger: string, version: string) +V2GetSchemaResponse (HttpMeta: HTTPMetadata, V2SchemaResponse: V2SchemaResponse) + V2SchemaResponse (data: V2Schema) + V2Schema (version: string, createdAt: date-time, data: V2SchemaData) +V2ListSchemasRequest (ledger: string, cursor: string, pageSize: integer ...) + Sort (enum: created_at) + Order (enum: asc, desc) +V2ListSchemasResponse (HttpMeta: HTTPMetadata, V2SchemasCursorResponse: V2SchemasCursorResponse) + V2SchemasCursorResponse (cursor: V2SchemasCursor) + V2SchemasCursor (data: array, hasMore: boolean, next: string ...) V2UpdateLedgerMetadataRequest (ledger: string, RequestBody: map) V2UpdateLedgerMetadataResponse (HttpMeta: HTTPMetadata, V2ErrorResponse: V2ErrorResponse) V2ErrorResponse (errorCode: V2ErrorsEnum, errorMessage: string, details: string) @@ -159,7 +172,7 @@ V2ReadStatsResponse (HttpMeta: HTTPMetadata, V2StatsResponse: V2StatsResponse) V2CountTransactionsRequest (ledger: string, pit: date-time, RequestBody: map) V2CountTransactionsResponse (HttpMeta: HTTPMetadata, Headers: map) V2ListTransactionsRequest (ledger: string, pageSize: integer, cursor: string ...) - Order (enum: effective) + QueryParamOrder (enum: effective) V2ListTransactionsResponse (HttpMeta: HTTPMetadata, V2TransactionsCursorResponse: V2TransactionsCursorResponse) V2TransactionsCursorResponse (cursor: class) V2TransactionsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) diff --git a/pkg/client/README.md b/pkg/client/README.md index 0c75097fd9..66ea7d344f 100644 --- a/pkg/client/README.md +++ b/pkg/client/README.md @@ -130,6 +130,9 @@ func main() { * [ListLedgers](docs/sdks/v2/README.md#listledgers) - List ledgers * [GetLedger](docs/sdks/v2/README.md#getledger) - Get a ledger * [CreateLedger](docs/sdks/v2/README.md#createledger) - Create a ledger +* [InsertSchema](docs/sdks/v2/README.md#insertschema) - Insert or update a schema for a ledger +* [GetSchema](docs/sdks/v2/README.md#getschema) - Get a schema for a ledger by version +* [ListSchemas](docs/sdks/v2/README.md#listschemas) - List all schemas for a ledger * [UpdateLedgerMetadata](docs/sdks/v2/README.md#updateledgermetadata) - Update ledger metadata * [DeleteLedgerMetadata](docs/sdks/v2/README.md#deleteledgermetadata) - Delete ledger metadata by key * [GetLedgerInfo](docs/sdks/v2/README.md#getledgerinfo) - Get information about a ledger diff --git a/pkg/client/docs/models/components/v2schema.md b/pkg/client/docs/models/components/v2schema.md new file mode 100644 index 0000000000..89a435b87d --- /dev/null +++ b/pkg/client/docs/models/components/v2schema.md @@ -0,0 +1,12 @@ +# V2Schema + +Complete schema structure with metadata + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `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 | +| `Data` | [components.V2SchemaData](../../models/components/v2schemadata.md) | :heavy_check_mark: | Schema data structure for ledger schemas | | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2schemadata.md b/pkg/client/docs/models/components/v2schemadata.md new file mode 100644 index 0000000000..94f1ef0df1 --- /dev/null +++ b/pkg/client/docs/models/components/v2schemadata.md @@ -0,0 +1,9 @@ +# V2SchemaData + +Schema data structure for ledger schemas + + +## Fields + +| Field | Type | Required | Description | +| ----------- | ----------- | ----------- | ----------- | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2schemaresponse.md b/pkg/client/docs/models/components/v2schemaresponse.md new file mode 100644 index 0000000000..87bfae96ad --- /dev/null +++ b/pkg/client/docs/models/components/v2schemaresponse.md @@ -0,0 +1,8 @@ +# V2SchemaResponse + + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| `Data` | [components.V2Schema](../../models/components/v2schema.md) | :heavy_check_mark: | Complete schema structure with metadata | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2schemascursor.md b/pkg/client/docs/models/components/v2schemascursor.md new file mode 100644 index 0000000000..ddfc5ce725 --- /dev/null +++ b/pkg/client/docs/models/components/v2schemascursor.md @@ -0,0 +1,11 @@ +# V2SchemasCursor + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| `Data` | [][components.V2Schema](../../models/components/v2schema.md) | :heavy_check_mark: | N/A | +| `HasMore` | *bool* | :heavy_check_mark: | N/A | +| `Next` | **string* | :heavy_minus_sign: | N/A | +| `PageSize` | *int64* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2schemascursorresponse.md b/pkg/client/docs/models/components/v2schemascursorresponse.md new file mode 100644 index 0000000000..7bee2cd753 --- /dev/null +++ b/pkg/client/docs/models/components/v2schemascursorresponse.md @@ -0,0 +1,8 @@ +# V2SchemasCursorResponse + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | +| `Cursor` | [components.V2SchemasCursor](../../models/components/v2schemascursor.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/order.md b/pkg/client/docs/models/operations/order.md index 27ec199a5b..284a4e4bad 100644 --- a/pkg/client/docs/models/operations/order.md +++ b/pkg/client/docs/models/operations/order.md @@ -1,10 +1,11 @@ # Order -Deprecated: Use sort param +The sort order ## Values -| Name | Value | -| ---------------- | ---------------- | -| `OrderEffective` | effective | \ No newline at end of file +| Name | Value | +| ----------- | ----------- | +| `OrderAsc` | asc | +| `OrderDesc` | desc | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/queryparamorder.md b/pkg/client/docs/models/operations/queryparamorder.md new file mode 100644 index 0000000000..324d98c724 --- /dev/null +++ b/pkg/client/docs/models/operations/queryparamorder.md @@ -0,0 +1,10 @@ +# QueryParamOrder + +Deprecated: Use sort param + + +## Values + +| Name | Value | +| -------------------------- | -------------------------- | +| `QueryParamOrderEffective` | effective | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/sort.md b/pkg/client/docs/models/operations/sort.md new file mode 100644 index 0000000000..2b82380d70 --- /dev/null +++ b/pkg/client/docs/models/operations/sort.md @@ -0,0 +1,10 @@ +# Sort + +The field to sort by + + +## Values + +| Name | Value | +| --------------- | --------------- | +| `SortCreatedAt` | created_at | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2getschemarequest.md b/pkg/client/docs/models/operations/v2getschemarequest.md new file mode 100644 index 0000000000..abf205335f --- /dev/null +++ b/pkg/client/docs/models/operations/v2getschemarequest.md @@ -0,0 +1,9 @@ +# V2GetSchemaRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Version` | *string* | :heavy_check_mark: | Schema version. | v1.0.0 | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2getschemaresponse.md b/pkg/client/docs/models/operations/v2getschemaresponse.md new file mode 100644 index 0000000000..c63af652ee --- /dev/null +++ b/pkg/client/docs/models/operations/v2getschemaresponse.md @@ -0,0 +1,9 @@ +# V2GetSchemaResponse + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `V2SchemaResponse` | [*components.V2SchemaResponse](../../models/components/v2schemaresponse.md) | :heavy_minus_sign: | Schema retrieved successfully | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2insertschemarequest.md b/pkg/client/docs/models/operations/v2insertschemarequest.md new file mode 100644 index 0000000000..fbb3f0809c --- /dev/null +++ b/pkg/client/docs/models/operations/v2insertschemarequest.md @@ -0,0 +1,10 @@ +# V2InsertSchemaRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Version` | *string* | :heavy_check_mark: | Schema version. | v1.0.0 | +| `V2SchemaData` | [components.V2SchemaData](../../models/components/v2schemadata.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2insertschemaresponse.md b/pkg/client/docs/models/operations/v2insertschemaresponse.md new file mode 100644 index 0000000000..48b7b0e2ac --- /dev/null +++ b/pkg/client/docs/models/operations/v2insertschemaresponse.md @@ -0,0 +1,8 @@ +# V2InsertSchemaResponse + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2listschemasrequest.md b/pkg/client/docs/models/operations/v2listschemasrequest.md new file mode 100644 index 0000000000..cafee09abc --- /dev/null +++ b/pkg/client/docs/models/operations/v2listschemasrequest.md @@ -0,0 +1,12 @@ +# V2ListSchemasRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Cursor` | **string* | :heavy_minus_sign: | The pagination cursor value | | +| `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page | | +| `Sort` | [*operations.Sort](../../models/operations/sort.md) | :heavy_minus_sign: | The field to sort by | | +| `Order` | [*operations.Order](../../models/operations/order.md) | :heavy_minus_sign: | The sort order | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2listschemasresponse.md b/pkg/client/docs/models/operations/v2listschemasresponse.md new file mode 100644 index 0000000000..a151423a0b --- /dev/null +++ b/pkg/client/docs/models/operations/v2listschemasresponse.md @@ -0,0 +1,9 @@ +# V2ListSchemasResponse + + +## Fields + +| Field | Type | Required | Description | +| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `V2SchemasCursorResponse` | [*components.V2SchemasCursorResponse](../../models/components/v2schemascursorresponse.md) | :heavy_minus_sign: | Schemas retrieved successfully | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2listtransactionsrequest.md b/pkg/client/docs/models/operations/v2listtransactionsrequest.md index 28d0dcd131..2fd3528b7b 100644 --- a/pkg/client/docs/models/operations/v2listtransactionsrequest.md +++ b/pkg/client/docs/models/operations/v2listtransactionsrequest.md @@ -10,7 +10,7 @@ | `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.Order](../../models/operations/order.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 | | +| ~~`Order`~~ | [*operations.QueryParamOrder](../../models/operations/queryparamorder.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` | map[string]*any* | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/sdks/v2/README.md b/pkg/client/docs/sdks/v2/README.md index e2defe62bf..b7da705ac6 100644 --- a/pkg/client/docs/sdks/v2/README.md +++ b/pkg/client/docs/sdks/v2/README.md @@ -8,6 +8,9 @@ * [ListLedgers](#listledgers) - List ledgers * [GetLedger](#getledger) - Get a ledger * [CreateLedger](#createledger) - Create a ledger +* [InsertSchema](#insertschema) - Insert or update a schema for a ledger +* [GetSchema](#getschema) - Get a schema for a ledger by version +* [ListSchemas](#listschemas) - List all schemas for a ledger * [UpdateLedgerMetadata](#updateledgermetadata) - Update ledger metadata * [DeleteLedgerMetadata](#deleteledgermetadata) - Delete ledger metadata by key * [GetLedgerInfo](#getledgerinfo) - Get information about a ledger @@ -231,6 +234,186 @@ func main() { | sdkerrors.V2ErrorResponse | default | application/json | | sdkerrors.SDKError | 4XX, 5XX | \*/\* | +## InsertSchema + +Insert or update a schema for 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.InsertSchema(ctx, operations.V2InsertSchemaRequest{ + Ledger: "ledger001", + Version: "v1.0.0", + V2SchemaData: components.V2SchemaData{}, + }) + if err != nil { + log.Fatal(err) + } + if res != 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.V2InsertSchemaRequest](../../models/operations/v2insertschemarequest.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.V2InsertSchemaResponse](../../models/operations/v2insertschemaresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------------- | ------------------------- | ------------------------- | +| sdkerrors.V2ErrorResponse | default | application/json | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + +## GetSchema + +Get a schema for a ledger by version + +### 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.GetSchema(ctx, operations.V2GetSchemaRequest{ + Ledger: "ledger001", + Version: "v1.0.0", + }) + if err != nil { + log.Fatal(err) + } + if res.V2SchemaResponse != 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.V2GetSchemaRequest](../../models/operations/v2getschemarequest.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.V2GetSchemaResponse](../../models/operations/v2getschemaresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------------- | ------------------------- | ------------------------- | +| sdkerrors.V2ErrorResponse | default | application/json | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + +## ListSchemas + +List all schemas for 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.ListSchemas(ctx, operations.V2ListSchemasRequest{ + Ledger: "ledger001", + }) + if err != nil { + log.Fatal(err) + } + if res.V2SchemasCursorResponse != 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.V2ListSchemasRequest](../../models/operations/v2listschemasrequest.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.V2ListSchemasResponse](../../models/operations/v2listschemasresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------------- | ------------------------- | ------------------------- | +| sdkerrors.V2ErrorResponse | default | application/json | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + ## UpdateLedgerMetadata Update ledger metadata diff --git a/pkg/client/models/components/v2schema.go b/pkg/client/models/components/v2schema.go new file mode 100644 index 0000000000..ec6f8c193c --- /dev/null +++ b/pkg/client/models/components/v2schema.go @@ -0,0 +1,50 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +import ( + "github.com/formancehq/ledger/pkg/client/internal/utils" + "time" +) + +// V2Schema - Complete schema structure with metadata +type V2Schema struct { + // Schema version + Version string `json:"version"` + // Schema creation timestamp + CreatedAt time.Time `json:"createdAt"` + // Schema data structure for ledger schemas + Data V2SchemaData `json:"data"` +} + +func (v V2Schema) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V2Schema) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, false); err != nil { + return err + } + return nil +} + +func (o *V2Schema) GetVersion() string { + if o == nil { + return "" + } + return o.Version +} + +func (o *V2Schema) GetCreatedAt() time.Time { + if o == nil { + return time.Time{} + } + return o.CreatedAt +} + +func (o *V2Schema) GetData() V2SchemaData { + if o == nil { + return V2SchemaData{} + } + return o.Data +} diff --git a/pkg/client/models/components/v2schemadata.go b/pkg/client/models/components/v2schemadata.go new file mode 100644 index 0000000000..2adb8ad4b2 --- /dev/null +++ b/pkg/client/models/components/v2schemadata.go @@ -0,0 +1,7 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +// V2SchemaData - Schema data structure for ledger schemas +type V2SchemaData struct { +} diff --git a/pkg/client/models/components/v2schemaresponse.go b/pkg/client/models/components/v2schemaresponse.go new file mode 100644 index 0000000000..6f4737c486 --- /dev/null +++ b/pkg/client/models/components/v2schemaresponse.go @@ -0,0 +1,15 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V2SchemaResponse struct { + // Complete schema structure with metadata + Data V2Schema `json:"data"` +} + +func (o *V2SchemaResponse) GetData() V2Schema { + if o == nil { + return V2Schema{} + } + return o.Data +} diff --git a/pkg/client/models/components/v2schemascursor.go b/pkg/client/models/components/v2schemascursor.go new file mode 100644 index 0000000000..a820304bbd --- /dev/null +++ b/pkg/client/models/components/v2schemascursor.go @@ -0,0 +1,38 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V2SchemasCursor struct { + Data []V2Schema `json:"data"` + HasMore bool `json:"hasMore"` + Next *string `json:"next,omitempty"` + PageSize int64 `json:"pageSize"` +} + +func (o *V2SchemasCursor) GetData() []V2Schema { + if o == nil { + return []V2Schema{} + } + return o.Data +} + +func (o *V2SchemasCursor) GetHasMore() bool { + if o == nil { + return false + } + return o.HasMore +} + +func (o *V2SchemasCursor) GetNext() *string { + if o == nil { + return nil + } + return o.Next +} + +func (o *V2SchemasCursor) GetPageSize() int64 { + if o == nil { + return 0 + } + return o.PageSize +} diff --git a/pkg/client/models/components/v2schemascursorresponse.go b/pkg/client/models/components/v2schemascursorresponse.go new file mode 100644 index 0000000000..408e1f0b69 --- /dev/null +++ b/pkg/client/models/components/v2schemascursorresponse.go @@ -0,0 +1,14 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V2SchemasCursorResponse struct { + Cursor V2SchemasCursor `json:"cursor"` +} + +func (o *V2SchemasCursorResponse) GetCursor() V2SchemasCursor { + if o == nil { + return V2SchemasCursor{} + } + return o.Cursor +} diff --git a/pkg/client/models/operations/v2getschema.go b/pkg/client/models/operations/v2getschema.go new file mode 100644 index 0000000000..dfb6e505b5 --- /dev/null +++ b/pkg/client/models/operations/v2getschema.go @@ -0,0 +1,48 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/ledger/pkg/client/models/components" +) + +type V2GetSchemaRequest struct { + // Name of the ledger. + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + // Schema version. + Version string `pathParam:"style=simple,explode=false,name=version"` +} + +func (o *V2GetSchemaRequest) GetLedger() string { + if o == nil { + return "" + } + return o.Ledger +} + +func (o *V2GetSchemaRequest) GetVersion() string { + if o == nil { + return "" + } + return o.Version +} + +type V2GetSchemaResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Schema retrieved successfully + V2SchemaResponse *components.V2SchemaResponse +} + +func (o *V2GetSchemaResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V2GetSchemaResponse) GetV2SchemaResponse() *components.V2SchemaResponse { + if o == nil { + return nil + } + return o.V2SchemaResponse +} diff --git a/pkg/client/models/operations/v2insertschema.go b/pkg/client/models/operations/v2insertschema.go new file mode 100644 index 0000000000..2b6356c7c3 --- /dev/null +++ b/pkg/client/models/operations/v2insertschema.go @@ -0,0 +1,47 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/ledger/pkg/client/models/components" +) + +type V2InsertSchemaRequest struct { + // Name of the ledger. + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + // Schema version. + Version string `pathParam:"style=simple,explode=false,name=version"` + V2SchemaData components.V2SchemaData `request:"mediaType=application/json"` +} + +func (o *V2InsertSchemaRequest) GetLedger() string { + if o == nil { + return "" + } + return o.Ledger +} + +func (o *V2InsertSchemaRequest) GetVersion() string { + if o == nil { + return "" + } + return o.Version +} + +func (o *V2InsertSchemaRequest) GetV2SchemaData() components.V2SchemaData { + if o == nil { + return components.V2SchemaData{} + } + return o.V2SchemaData +} + +type V2InsertSchemaResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` +} + +func (o *V2InsertSchemaResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} diff --git a/pkg/client/models/operations/v2listschemas.go b/pkg/client/models/operations/v2listschemas.go new file mode 100644 index 0000000000..36cb6d80a9 --- /dev/null +++ b/pkg/client/models/operations/v2listschemas.go @@ -0,0 +1,140 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "encoding/json" + "fmt" + "github.com/formancehq/ledger/pkg/client/internal/utils" + "github.com/formancehq/ledger/pkg/client/models/components" +) + +// Sort - The field to sort by +type Sort string + +const ( + SortCreatedAt Sort = "created_at" +) + +func (e Sort) ToPointer() *Sort { + return &e +} +func (e *Sort) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "created_at": + *e = Sort(v) + return nil + default: + return fmt.Errorf("invalid value for Sort: %v", v) + } +} + +// Order - The sort order +type Order string + +const ( + OrderAsc Order = "asc" + OrderDesc Order = "desc" +) + +func (e Order) ToPointer() *Order { + return &e +} +func (e *Order) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "asc": + fallthrough + case "desc": + *e = Order(v) + return nil + default: + return fmt.Errorf("invalid value for Order: %v", v) + } +} + +type V2ListSchemasRequest struct { + // Name of the ledger. + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + // The pagination cursor value + Cursor *string `queryParam:"style=form,explode=true,name=cursor"` + // The maximum number of results to return per page + PageSize *int64 `default:"15" queryParam:"style=form,explode=true,name=pageSize"` + // The field to sort by + Sort *Sort `default:"created_at" queryParam:"style=form,explode=true,name=sort"` + // The sort order + Order *Order `default:"desc" queryParam:"style=form,explode=true,name=order"` +} + +func (v V2ListSchemasRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V2ListSchemasRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, false); err != nil { + return err + } + return nil +} + +func (o *V2ListSchemasRequest) GetLedger() string { + if o == nil { + return "" + } + return o.Ledger +} + +func (o *V2ListSchemasRequest) GetCursor() *string { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *V2ListSchemasRequest) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *V2ListSchemasRequest) GetSort() *Sort { + if o == nil { + return nil + } + return o.Sort +} + +func (o *V2ListSchemasRequest) GetOrder() *Order { + if o == nil { + return nil + } + return o.Order +} + +type V2ListSchemasResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Schemas retrieved successfully + V2SchemasCursorResponse *components.V2SchemasCursorResponse +} + +func (o *V2ListSchemasResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V2ListSchemasResponse) GetV2SchemasCursorResponse() *components.V2SchemasCursorResponse { + if o == nil { + return nil + } + return o.V2SchemasCursorResponse +} diff --git a/pkg/client/models/operations/v2listtransactions.go b/pkg/client/models/operations/v2listtransactions.go index 1de950d71d..8983d41aa1 100644 --- a/pkg/client/models/operations/v2listtransactions.go +++ b/pkg/client/models/operations/v2listtransactions.go @@ -10,27 +10,27 @@ import ( "time" ) -// Order - Deprecated: Use sort param -type Order string +// QueryParamOrder - Deprecated: Use sort param +type QueryParamOrder string const ( - OrderEffective Order = "effective" + QueryParamOrderEffective QueryParamOrder = "effective" ) -func (e Order) ToPointer() *Order { +func (e QueryParamOrder) ToPointer() *QueryParamOrder { return &e } -func (e *Order) UnmarshalJSON(data []byte) error { +func (e *QueryParamOrder) UnmarshalJSON(data []byte) error { var v string if err := json.Unmarshal(data, &v); err != nil { return err } switch v { case "effective": - *e = Order(v) + *e = QueryParamOrder(v) return nil default: - return fmt.Errorf("invalid value for Order: %v", v) + return fmt.Errorf("invalid value for QueryParamOrder: %v", v) } } @@ -51,8 +51,8 @@ type V2ListTransactionsRequest struct { // Deprecated: Use sort param // // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. - Order *Order `queryParam:"style=form,explode=true,name=order"` - Reverse *bool `queryParam:"style=form,explode=true,name=reverse"` + Order *QueryParamOrder `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`. // @@ -106,7 +106,7 @@ func (o *V2ListTransactionsRequest) GetPit() *time.Time { return o.Pit } -func (o *V2ListTransactionsRequest) GetOrder() *Order { +func (o *V2ListTransactionsRequest) GetOrder() *QueryParamOrder { if o == nil { return nil } diff --git a/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log b/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log index 5da1bd87e5..d569ce5b48 100644 --- a/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log +++ b/pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log @@ -101,6 +101,19 @@ V2GetLedgerResponse (HttpMeta: HTTPMetadata, V2GetLedgerResponse: V2GetLedgerRes V2CreateLedgerRequest (ledger: string, V2CreateLedgerRequest: V2CreateLedgerRequest) V2CreateLedgerRequest (bucket: string, metadata: map, features: map) V2CreateLedgerResponse (HttpMeta: HTTPMetadata) +V2InsertSchemaRequest (ledger: string, version: string, V2SchemaData: V2SchemaData) + V2SchemaData (empty) +V2InsertSchemaResponse (HttpMeta: HTTPMetadata) +V2GetSchemaRequest (ledger: string, version: string) +V2GetSchemaResponse (HttpMeta: HTTPMetadata, V2SchemaResponse: V2SchemaResponse) + V2SchemaResponse (data: V2Schema) + V2Schema (version: string, createdAt: date-time, data: V2SchemaData) +V2ListSchemasRequest (ledger: string, cursor: string, pageSize: integer ...) + Sort (enum: created_at) + Order (enum: asc, desc) +V2ListSchemasResponse (HttpMeta: HTTPMetadata, V2SchemasCursorResponse: V2SchemasCursorResponse) + V2SchemasCursorResponse (cursor: V2SchemasCursor) + V2SchemasCursor (data: array, hasMore: boolean, next: string ...) V2UpdateLedgerMetadataRequest (ledger: string, RequestBody: map) V2UpdateLedgerMetadataResponse (HttpMeta: HTTPMetadata, V2ErrorResponse: V2ErrorResponse) V2ErrorResponse (errorCode: V2ErrorsEnum, errorMessage: string, details: string) @@ -159,7 +172,7 @@ V2ReadStatsResponse (HttpMeta: HTTPMetadata, V2StatsResponse: V2StatsResponse) V2CountTransactionsRequest (ledger: string, pit: date-time, RequestBody: map) V2CountTransactionsResponse (HttpMeta: HTTPMetadata, Headers: map) V2ListTransactionsRequest (ledger: string, pageSize: integer, cursor: string ...) - Order (enum: effective) + QueryParamOrder (enum: effective) V2ListTransactionsResponse (HttpMeta: HTTPMetadata, V2TransactionsCursorResponse: V2TransactionsCursorResponse) V2TransactionsCursorResponse (cursor: class) V2TransactionsCursorResponseCursor (pageSize: integer, hasMore: boolean, previous: string ...) diff --git a/pkg/client/v2.go b/pkg/client/v2.go index 7dc160c1e4..d7100b55a5 100644 --- a/pkg/client/v2.go +++ b/pkg/client/v2.go @@ -653,6 +653,621 @@ func (s *V2) CreateLedger(ctx context.Context, request operations.V2CreateLedger } +// InsertSchema - Insert or update a schema for a ledger +func (s *V2) InsertSchema(ctx context.Context, request operations.V2InsertSchemaRequest, opts ...operations.Option) (*operations.V2InsertSchemaResponse, 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}/schema/{version}", 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: "v2InsertSchema", + OAuth2Scopes: []string{"ledger:read", "ledger:write"}, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "V2SchemaData", "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.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.V2InsertSchemaResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 204: + 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 + +} + +// GetSchema - Get a schema for a ledger by version +func (s *V2) GetSchema(ctx context.Context, request operations.V2GetSchemaRequest, opts ...operations.Option) (*operations.V2GetSchemaResponse, 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}/schema/{version}", 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: "v2GetSchema", + OAuth2Scopes: []string{"ledger:read", "ledger:read"}, + SecuritySource: s.sdkConfiguration.Security, + } + + 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, "GET", opURL, nil) + 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 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.V2GetSchemaResponse{ + 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 components.V2SchemaResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V2SchemaResponse = &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 + +} + +// ListSchemas - List all schemas for a ledger +func (s *V2) ListSchemas(ctx context.Context, request operations.V2ListSchemasRequest, opts ...operations.Option) (*operations.V2ListSchemasResponse, 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}/schema", 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: "v2ListSchemas", + OAuth2Scopes: []string{"ledger:read", "ledger:read"}, + SecuritySource: s.sdkConfiguration.Security, + } + + 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, "GET", opURL, nil) + 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 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.V2ListSchemasResponse{ + 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 components.V2SchemasCursorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V2SchemasCursorResponse = &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 + +} + // UpdateLedgerMetadata - Update ledger metadata func (s *V2) UpdateLedgerMetadata(ctx context.Context, request operations.V2UpdateLedgerMetadataRequest, opts ...operations.Option) (*operations.V2UpdateLedgerMetadataResponse, error) { o := operations.Options{} diff --git a/pkg/events/events.go b/pkg/events/events.go index f6d6f55c64..d4394c6ac1 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -7,5 +7,6 @@ const ( EventTypeCommittedTransactions = "COMMITTED_TRANSACTIONS" EventTypeSavedMetadata = "SAVED_METADATA" EventTypeRevertedTransaction = "REVERTED_TRANSACTION" - EventTypeDeletedMetadata = "DELETED_METADATA" + EventTypeDeletedMetadata = "DELETED_METADATA" + EventTypeUpdatedSchema = "UPDATED_SCHEMA" ) diff --git a/pkg/events/message.go b/pkg/events/message.go index 9c47d81fcd..c197f6b16a 100644 --- a/pkg/events/message.go +++ b/pkg/events/message.go @@ -72,3 +72,18 @@ func NewEventDeletedMetadata(deletedMetadata DeletedMetadata) publish.EventMessa Payload: deletedMetadata, } } + +type UpdatedSchema struct { + Ledger string `json:"ledger"` + Schema ledger.Schema `json:"schema"` +} + +func NewEventUpdatedSchema(updatedSchema UpdatedSchema) publish.EventMessage { + return publish.EventMessage{ + Date: time.Now().Time, + App: EventApp, + Version: EventVersion, + Type: EventTypeUpdatedSchema, + Payload: updatedSchema, + } +} diff --git a/test/e2e/api_schema_test.go b/test/e2e/api_schema_test.go new file mode 100644 index 0000000000..d86cd39433 --- /dev/null +++ b/test/e2e/api_schema_test.go @@ -0,0 +1,144 @@ +//go:build it + +package test_suite + +import ( + "github.com/formancehq/go-libs/v3/logging" + . "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/operations" + . "github.com/formancehq/ledger/pkg/testserver" + "github.com/formancehq/ledger/pkg/testserver/ginkgo" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Context("Ledger schema API tests", func() { + var ( + db = UseTemplatedDatabase() + ctx = logging.TestingContext() + ) + + testServer := ginkgo.DeferTestServer( + DeferMap(db, (*pgtesting.Database).ConnectionOptions), + testservice.WithInstruments( + testservice.DebugInstrumentation(debug), + 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()) + }) + + It("should return empty list when no schemas exist", func(specContext SpecContext) { + schemas, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.ListSchemas(ctx, operations.V2ListSchemasRequest{ + Ledger: "default", + }) + Expect(err).To(BeNil()) + Expect(schemas.V2SchemasCursorResponse.Cursor.Data).To(HaveLen(0)) + Expect(schemas.V2SchemasCursorResponse.Cursor.HasMore).To(BeFalse()) + Expect(schemas.V2SchemasCursorResponse.Cursor.PageSize).To(Equal(int64(15))) + }) + + When("inserting a schema", func() { + BeforeEach(func(specContext SpecContext) { + _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.InsertSchema(ctx, operations.V2InsertSchemaRequest{ + Ledger: "default", + Version: "v1.0.0", + }) + Expect(err).To(BeNil()) + }) + + It("should be able to read the schema", func(specContext SpecContext) { + schema, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.GetSchema(ctx, operations.V2GetSchemaRequest{ + Ledger: "default", + Version: "v1.0.0", + }) + Expect(err).To(BeNil()) + Expect(schema.V2SchemaResponse.Data.Version).To(Equal("v1.0.0")) + Expect(schema.V2SchemaResponse.Data.CreatedAt).ToNot(BeZero()) + }) + + When("inserting another version of the schema", func() { + BeforeEach(func(specContext SpecContext) { + _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.InsertSchema(ctx, operations.V2InsertSchemaRequest{ + Ledger: "default", + Version: "v2.0.0", + }) + Expect(err).To(BeNil()) + }) + + It("should be able to read both schema versions", func(specContext SpecContext) { + // Read v1.0.0 + schemaV1, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.GetSchema(ctx, operations.V2GetSchemaRequest{ + Ledger: "default", + Version: "v1.0.0", + }) + Expect(err).To(BeNil()) + Expect(schemaV1.V2SchemaResponse.Data.Version).To(Equal("v1.0.0")) + + // Read v2.0.0 + schemaV2, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.GetSchema(ctx, operations.V2GetSchemaRequest{ + Ledger: "default", + Version: "v2.0.0", + }) + Expect(err).To(BeNil()) + Expect(schemaV2.V2SchemaResponse.Data.Version).To(Equal("v2.0.0")) + }) + + It("should be able to list all schemas", func(specContext SpecContext) { + schemas, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.ListSchemas(ctx, operations.V2ListSchemasRequest{ + Ledger: "default", + }) + Expect(err).To(BeNil()) + Expect(schemas.V2SchemasCursorResponse.Cursor.Data).To(HaveLen(2)) + Expect(schemas.V2SchemasCursorResponse.Cursor.HasMore).To(BeFalse()) + Expect(schemas.V2SchemasCursorResponse.Cursor.PageSize).To(Equal(int64(15))) + + // Check that schemas are ordered by created_at DESC (newest first) + Expect(schemas.V2SchemasCursorResponse.Cursor.Data[0].Version).To(Equal("v2.0.0")) + Expect(schemas.V2SchemasCursorResponse.Cursor.Data[1].Version).To(Equal("v1.0.0")) + }) + }) + + When("trying to read a non-existent schema version", func() { + It("should return 404", func(specContext SpecContext) { + _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.GetSchema(ctx, operations.V2GetSchemaRequest{ + Ledger: "default", + Version: "non-existent", + }) + Expect(err).ToNot(BeNil()) + }) + }) + + When("trying to insert the same schema version again", func() { + It("should fail", func(specContext SpecContext) { + _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.InsertSchema(ctx, operations.V2InsertSchemaRequest{ + Ledger: "default", + Version: "v1.0.0", + }) + Expect(err).NotTo(BeNil()) + }) + }) + + It("should be able to list schemas", func(specContext SpecContext) { + schemas, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.ListSchemas(ctx, operations.V2ListSchemasRequest{ + Ledger: "default", + }) + Expect(err).To(BeNil()) + Expect(schemas.V2SchemasCursorResponse.Cursor.Data).To(HaveLen(1)) + Expect(schemas.V2SchemasCursorResponse.Cursor.Data[0].Version).To(Equal("v1.0.0")) + Expect(schemas.V2SchemasCursorResponse.Cursor.Data[0].CreatedAt).ToNot(BeZero()) + Expect(schemas.V2SchemasCursorResponse.Cursor.HasMore).To(BeFalse()) + Expect(schemas.V2SchemasCursorResponse.Cursor.PageSize).To(Equal(int64(15))) + }) + }) + }) +}) diff --git a/test/e2e/api_transactions_list_test.go b/test/e2e/api_transactions_list_test.go index e37c65384b..25dd6e0531 100644 --- a/test/e2e/api_transactions_list_test.go +++ b/test/e2e/api_transactions_list_test.go @@ -272,7 +272,7 @@ var _ = Context("Ledger transactions list API tests", func() { Context("with effective ordering", func() { BeforeEach(func() { //nolint:staticcheck - req.Order = pointer.For(operations.OrderEffective) + req.Order = pointer.For(operations.QueryParamOrderEffective) }) It("Should be ok, and returns transactions ordered by effective timestamp", func() { Expect(rsp.V2TransactionsCursorResponse.Cursor.PageSize).To(Equal(pageSize))